
В процессе своих исследований работы VPN‑протоколов в реальных интернет‑условиях я задался вопросом определения сетевой доступности эндпоинтов Hysteria2 и характера их ответа на активный пробинг. Протокол использует QUIC поверх UDP, а сам UDP не предусматривает ни установления соединения, ни подтверждений доставки датаграмм. Отвечать ли на поступившие данные — это уже дело протокола, который работает поверх него. Поэтому обычная UDP‑проба здесь могла мало что сказать, и изучение поведения эндпоинтов потребовало более глубокого подхода.
Естественно возникли следующие исследовательские вопросы:
каков будет ответ хоста на попытку корректного QUIC‑handshake на UDP‑порт, где «сидит» Hysteria2;
если QUIC‑соединение установится и будет согласован HTTP/3, то ответит ли сервер на обычный HTTP/3-запрос;
а как будет выглядеть подключение полноценного Hysteria2-клиента с действующими данными аутентификации? Где в трафике можно увидеть, что сервер его принял;
и наконец, как убедиться, что через установленное соединение действительно идут данные? Ведь нам в итоге нужно пользоваться сервисом, а не просто получить сообщение об успешном подключении.
Для этих экспериментов я воспользовался подпиской на один из коммерческих VPN‑сервисов, из конфигурации выделил Hysteria2-эндпоинты и поработал с ними при помощи спроектированных мной инструментов для сетевых исследований NetProbe — Windows‑ и Linux‑версий.
Как устроено исследование
Итак, для исследования я использовал два инструмента, у каждого была своя задача. Принцип следующий: сначала посмотреть, что вообще можно узнать о сервисе снаружи, имея адрес, порт и некоторые параметры из конфигурации подписки. Затем — подключиться полноценным Hysteria2-клиентом и разобраться, какие процессы происходят внутри при установлении легитимного соединения и передаче данных.
Для первой части использовался Windows NetProbe версии 0.10.2. Сначала он проверял, отвечает ли узел на ICMP Echo‑запросы (ping), затем отправлял обычную UDP‑пробу на заданный порт, потом пробовал установить обычное QUIC‑соединение и, если handshake проходил с согласованным ALPN h3, выполнял один HTTP/3-запрос GET. При этом сохранялись параметры TLS, сертификат и результат каждого этапа. Я проверил подключение с SNI из конфигурации и без него, сохранив одинаковое значение HTTP/3 authority — имя сервиса и порт, к которому обращён запрос (в нашем случае — domain_name:port).
Для второй части понадобился Linux NetProbe. Здесь уже запускался Hysteria2-клиент с действующими данными подписки, а параллельно записывался сетевой трафик в формате PCAPNG. Инструментированный Hysteria2-клиент логировал основные внутренние события и сохранял ключи внешней QUIC/TLS‑сессии. Это дало возможность расшифровать внешний QUIC‑обмен в TShark и сопоставить его с тем, что происходило внутри клиента, а при необходимости — разобрать отдельные эпизоды в Wireshark (например, аутентификацию Hysteria2 поверх HTTP/3).
После подключения выполнялась контрольная нагрузка: короткие параллельные запросы, загрузка файла с проверкой размера и SHA-256, затем наблюдение соединения в простое. Так можно было проверить и сам факт передачи данных, и связь этих операций с исследуемым соединением.
Получились два дополняющих друг друга взгляда. Внешние пробы показывают, на какие обращения сервис отвечает и до какого этапа удаётся дойти. Пакетный захват и события полноценного клиента помогают разобрать аутентификацию и последующую работу прокси.

В статье использованы Windows‑прогоны от 13 сентября 2026 года и Linux‑захват от 6 сентября. Они выполнены на однотипных узлах одного VPN‑сервиса через домашнее подключение; Linux работал в виртуальной машине VMware с NAT. Название сервиса и идентифицирующие данные обезличены. Внутренний HTTPS‑трафик контрольной нагрузки не расшифровывался.
От UDP‑молчания к установленному QUIC‑соединению
Начал я с самого простого обращения к целевому UDP‑порту. NetProbe отправил датаграмму с простым текстом (всего 14 байт) и в течение трёх секунд ожидал ответ. Никакой протокольной структуры тут нет: это обычная UDP‑проба.
Ответа не последовало. В отчёте появился статус TIMEOUT_UNKNOWN. Собственно, на этом возможности такой пробы и заканчиваются: мы знаем, что за отведённое время ответ не получен. Считать порт закрытым на этом основании нельзя, сервер мог просто отбросить непонятное ему сообщение.
Следующим шагом я оставил тот же адрес и порт, но изменил содержание обращения. Теперь NetProbe запускал обычный QUIC‑клиент: отправлял QUIC Initial с TLS ClientHello и предлагал прикладной протокол h3 через ALPN. Данные аутентификации Hysteria2 в этой проверке не использовались. Это всё ещё внешний активный пробинг, а не попытка подключения к Hysteria‑endpoint по подписке.
Здесь я немного остановлюсь на устройстве QUIC. TLS 1.3 в нём участвует в установлении соединения и выработке ключей, но привычного слоя TLS records здесь нет. Сообщения handshake, включая ClientHello и ServerHello, передаются в QUIC‑фреймах CRYPTO; доставкой этих данных и защитой пакетов занимается QUIC. Поэтому сертификат сервера мы получаем в ходе TLS‑handshake внутри QUIC, хотя весь обмен идёт поверх UDP. Это взаимодействие описано в RFC 9001.
На этот раз сервис ответил, и клиентский стек сообщил о завершении handshake. В отчёте сохранились QUIC_V1, TLSv1.3, согласованный ALPN h3 и набор шифров TLS_AES_256_GCM_SHA384. Был получен и сертификат сервера; проверка доверия к нему в этом диагностическом режиме не выполнялась.
Обращение к исследуемому порту |
Наблюдение |
Generic UDP: 14 байт, ожидание 3 секунды |
Ответ не получен — TIMEOUT_UNKNOWN |
QUIC с SNI из конфигурации / без SNI |
Handshake завершён, согласован h3 |
Отсутствие SNI в парной проверке результат handshake не изменило: сервер предъявил тот же сертификат, что подтверждается SHA-256.
Итак, на одном порту мы получили две разные картины. Произвольная датаграмма осталась без ответа, а корректное QUIC‑обращение привело к установлению соединения. При этом до аутентификации Hysteria2 мы пока не дошли.

Теперь появился следующий вопрос: раз сервер согласовал h3, что он сделает с обычным HTTP/3-запросом GET /?
HTTP/3 согласован: что произошло после handshake
После успешного handshake NetProbe перешёл к следующему шагу — обычному HTTP/3-запросу GET /. В поле :authority использовалось имя из конфигурации с указанием порта. Никаких данных подписки или специальных заголовков Hysteria2 в запросе не было.
Продолжение оказалось коротким. Этап HTTP/3 занял около 102 мс и закончился закрытием соединения со стороны сервера. Ни HTTP‑статуса, ни заголовков ответа, ни тела клиент не получил. Этот результат виден в нижней части отчёта на рисунке 2: GET TRANSPORT_SUBMITTED, затем HTTP/3 CONNECTION_TERMINATED.
Что именно пришло от сервера? QUIC‑фрейм CONNECTION_CLOSE с прикладным кодом 256, то есть 0x100. В HTTP/3 это H3_NO_ERROR — завершение соединения без указания ошибки (RFC 9114, раздел 8.1). Это не HTTP‑ответ с кодом 256 и не подтверждение успешного выполнения запроса. По существу сервер сообщил: соединение закрываю, конкретную ошибку не заявляю.
Со стороны клиента отправка GET была зарегистрирована как передача локальному QUIC‑транспорту. Успел ли сервер разобрать запрос и что именно решил после этого — отчёт не показывает. Но сам результат вполне определённый: защищённое соединение установилось, а обычного веб‑ответа мы через него не получили.
Выбранные поля JSON‑отчёта NetProbe. Зарегистрировано закрытие соединения удалённой стороной на этапе ожидания HTTP/3-ответа.
{ "http3": { "state": "CONNECTION_TERMINATED", "request": { "state": "TRANSPORT_SUBMITTED", "method": "GET", "path": "/", "peer_receipt": "NOT_ASSESSED" }, "response": { "final_status_code": null, "final_response_received": false, "received_body_bytes": 0 } }, "transport": { "connection_close": { "observed": true, "origin": "PEER", "peer_close_confirmed": true, "code_space": "HTTP3_APPLICATION", "error_code": 256, "close_frame_type": 29, "reason": "Connection was closed by the server", "stage": "HTTP3_RESPONSE" } } }
Интересно сопоставить это с замыслом Hysteria2. В спецификации протокола, разделе Authentication & HTTP/3 masquerading, предусмотрено, что для клиента без подходящих данных аутентификации сервер ведёт себя как обычный HTTP/3-веб‑сервер. В частности, разработчики рекомендуют отдавать реальный контент или проксировать запросы к другому сайту. Таким образом, посторонний клиент должен увидеть вполне обычное веб‑поведение.
В нашей попытке такой картины не случилось: вместо страницы или хотя бы HTTP‑ошибки пришло закрытие QUIC‑соединения. Моя рабочая гипотеза — на исследуемом узле обслуживание обычных веб‑запросов не настроено либо ограничено, и такое обращение заканчивается простым закрытием. Для инфраструктуры, используемой прежде всего как VPN, это выглядит практическим объяснением.
Полноценный Hysteria2-клиент должен начать прикладной обмен иначе: отправить HTTP/3-запрос POST /auth с данными аутентификации в заголовке Hysteria-Auth. При успешной проверке сервер отвечает статусом 233, после чего принимает прокси‑запросы в этом QUIC‑соединении. Именно такой порядок задаёт спецификация Hysteria2.
До этой точки внешний пробинг нас не довёл — аутентифицироваться он и не пытался. Поэтому следующим шагом я подключился полноценным Hysteria2-клиентом и посмотрел, что меняется, когда у клиента есть действующие данные подписки.
Под шифрованием: где появляется Hysteria2
Теперь перейдём к подключению полноценного Hysteria2-клиента. Для этого опыта я использовал Linux NetProbe с инструментированным клиентом и действующими данными подписки. Здесь у нас уже есть пакетный захват и возможность сопоставить его с внутренними событиями клиента.
Сначала откроем захват в Wireshark без сессионных ключей. Здесь видны UDP‑датаграммы, начальный обмен QUIC и последующие пакеты с защищённым содержимым. Можно проследить направления, размеры и интервалы между пакетами. Начальный ClientHello тоже доступен для разбора: защита QUIC Initial сама по себе не скрывает его от наблюдателя. Но прикладное содержимое пакетов 1-RTT уже зашифровано. По такому представлению нельзя прочитать ни запрос аутентификации, ни ответ на него.

Инструментированный клиент сохранил ключи этой сессии в keylog. Подключаем его в Wireshark и возвращаемся к тому же участку захвата. Сам файл трафика не изменился, но теперь анализатор может расшифровать защищённое содержимое и разобрать прикладной обмен.
И вот здесь появляется Hysteria2. Внутри QUIC‑потока виден HTTP/3-запрос POST /auth. Его заголовки содержат поля, предусмотренные протоколом:
Hysteria-Auth— данные аутентификации клиента. Их значение на иллюстрации скрыто;Hysteria-CC-RX— заявленная клиентом максимальная скорость приёма в байтах в секунду;Hysteria-Padding— поле заполнения, предназначенное для изменения размера сообщения.
Это уже существенно отличается от нашего предыдущего GET /. Полноценный клиент обращается к специальному пути и передаёт данные, по которым сервер должен решить, разрешать ли работу прокси. Такой обмен описан в спецификации Hysteria2.
В ответ приходит HTTP/3-статус 233. Для Hysteria2 это подтверждение успешной аутентификации. В ответе также присутствует Hysteria-UDP: true — сервер сообщает о поддержке пересылки UDP.
Запрос и ответ относятся к одному клиентскому двунаправленному QUIC‑потоку — stream 0. Поэтому перед нами связанная пара сообщений: клиент передал запрос аутентификации, сервер подтвердил её результат. В модели выбранного захвата опорными кадрами для этих сообщений стали 13 и 17.

POST /auth (кадр 13) и ответ 233 (кадр 17) в одном потоке, Stream ID 0. Аутентификационные данные скрыты. Hysteria-CC-RX — заявленная клиентом максимальная скорость приёма в байтах в секунду; значение 0 в нашем запросе означает, что клиент её не указывает. Здесь полезно посмотреть, как именно сообщения вложены друг в друга. UDP переносит QUIC‑пакеты; внутри расшифрованных пакетов находятся фреймы STREAM, несущие данные потока. Из этих данных Wireshark собирает HTTP/3-фреймы и разбирает HEADERS. А уже в заголовках мы видим признаки аутентификации Hysteria2.
При этом один HTTP/3-запрос вовсе не обязан помещаться в одну UDP‑датаграмму. Данные потока могут приходить частями, а один QUIC‑пакет — содержать несколько фреймов. Поэтому номер кадра, к которому Wireshark привязал разобранное сообщение, не следует автоматически понимать как «весь запрос передан только этим пакетом».
Теперь разница между двумя опытами становится предметной. Внешняя QUIC‑проба установила защищённое соединение, но обычный GET закончился закрытием без HTTP‑ответа. Полноценный клиент передал предусмотренный Hysteria2 запрос и получил подтверждение аутентификации.
Это важный рубеж, но ещё не вся проверка. Сервер принял клиента — теперь нужно убедиться, что через это соединение действительно передаются данные.
Проверяем реальную передачу данных
Аутентификация прошла. Теперь посмотрим, как через это соединение передаются данные. Для этого я последовательно дал клиенту три вида нагрузки: несколько коротких обращений с параллельным выполнением, загрузку файла и затем три минуты простоя.
Но сначала — одна особенность Hysteria2, которая становится заметна именно здесь. Для аутентификации клиент использовал HTTP/3, однако последующие обращения к TCP‑сервисам устроены иначе. Для каждого проксируемого TCP‑соединения он открывает отдельный двунаправленный поток QUIC и отправляет в нём собственный запрос Hysteria2: куда нужно подключиться. Сервер возвращает результат подключения, после чего через этот поток передаются данные. Формат этого обмена описан в спецификации Hysteria2.
То есть после POST /auth → 233 мы не должны ожидать бесконечной последовательности HTTP/3-запросов. Внутри QUIC уже работает другой прикладной обмен. При этом само QUIC‑соединение сохраняется: новые подключения к целевым ресурсам обслуживаются отдельными потоками внутри него.

Первый этап — восемь коротких запросов с параллельным выполнением. Такой сценарий ближе к обычной работе приложения, которое обращается сразу к нескольким ресурсам. Здесь было интересно посмотреть, как клиент открывает прокси‑соединения и как эти обращения распределяются по потокам QUIC.
Одного списка пакетов для этого неудобно — много листать. Поэтому NetProbe сопоставлял события инструментированного клиента с выполняемой нагрузкой: какая операция запущена, какой поток ей соответствует, с каким результатом завершилось подключение. Так отдельные STREAM в захвате получали понятный смысл — становилось видно, какую работу они обслуживают.
Следом я запустил загрузку файла объёмом 4 МиБ. Здесь уже проверялась непрерывная передача данных. Файл был получен полностью, его размер и SHA-256 совпали с ожидаемыми. Для нашего опыта это и было практическим результатом: через исследуемое соединение удалось получить заданные данные без искажения.
Наконец, после нагрузки клиент оставался подключённым ещё 180 секунд, уже без новых пользовательских запросов. Этот участок нужен, чтобы посмотреть на поведение соединения в простое, когда обмен больше не определяется загрузкой файла. «Тишина» здесь означает отсутствие нашей нагрузки: служебные пакеты при этом вполне могут продолжать ходить.

В результате мы прошли весь путь от принятой аутентификации до передачи полезных данных и смогли связать её с конкретными потоками соединения. При этом внутренний HTTPS контрольных обращений мы не расшифровывали. Сессионные ключи открыли внешний слой QUIC, а содержимое HTTPS осталось под собственным TLS‑шифрованием.
Что удалось увидеть — и зачем проверять несколько уровней
Подводя итоги. Начали мы с UDP‑порта, который не ответил на обычную пробу. Затем установили QUIC‑соединение, попробовали обратиться к серверу по HTTP/3, а уже полноценным Hysteria2-клиентом прошли аутентификацию и передали контрольную нагрузку. По мере продвижения картина менялась: первоначальное «молчит» постепенно раскрылось в несколько вполне конкретных состояний.
Этап |
Наблюдение |
Что это означает |
|---|---|---|
Обычная UDP‑проба |
За отведённое время ответ не получен |
На такое обращение ответа нет. Состояние сервиса остаётся неизвестным |
QUIC/TLS |
Handshake завершён, согласован ALPN |
Сервер доступен для установления защищённого QUIC‑соединения |
Обычный HTTP/3 |
Сервер закрыл соединение без финального HTTP‑ответа |
QUIC‑соединение установилось, но получить веб‑ответ в этой попытке не удалось |
Аутентификация Hysteria2 |
|
Сервер принял аутентификацию клиента |
Контрольная нагрузка |
Выполнены короткие обращения, файл получен с ожидаемым размером и SHA-256 |
В исследованном сеансе работал путь передачи данных через прокси |
Простой |
После нагрузки соединение наблюдалось ещё 180 секунд |
Получен отдельный участок для анализа поведения без новых пользовательских запросов |
Для инженера здесь, пожалуй, самое полезное — это сама последовательность проверок. Соединение многослойное, и успешный результат на одном уровне ещё оставляет вопросы к следующему. Сервер может отвечать на QUIC‑handshake, но не обслужить обычный HTTP/3-запрос. А успешная аутентификация прокси ещё не рассказывает, удаётся ли через него достичь нужного ресурса.
У каждой такой границы свои возможные причины сбоя: фильтрация по пути, настройки сервера, ограничения доступа, проблемы выхода в Интернет. Сохранённые промежуточные результаты помогают понять, с какого участка начинать разбор. Само по себе место остановки, конечно, ещё не указывает виновника — например, приписывать любой таймаут DPI было бы слишком просто.
Для VPN‑продукта отсюда следует вполне прикладная вещь. Прежде чем показывать пользователю «подключено», полезно проверить короткое обращение к контрольному ресурсу именно через созданный туннель, с получением ожидаемого ответа. Такую проверку стоит повторять и во время работы: однажды установленное соединение может остаться в интерфейсе зелёным уже после потери рабочего пути. Пинг самого VPN‑сервера этой задачи не решает.
Для исследователя ценность такого подхода — в возможности связать пользовательское действие с поведением протокола. За неудачной загрузкой оказываются конкретные этапы: установление транспорта, аутентификация, открытие прокси‑потока, передача данных. С этой картиной уже можно предметно сравнивать конфигурации и условия доступа, а не только собирать результаты «работает / не работает».
Наши внешние пробы и полноценное подключение выполнялись на однотипных узлах сервиса в разное время. Серверной телеметрии у нас не было, внутренний HTTPS мы не расшифровывали. Поэтому здесь показан конкретный опыт работы и способ его разобрать.