Привет, Habr! Задумывались ли вы когда‑нибудь, почему ваш написанный парсер, который досконально копирует заголовки браузера, вдруг наглухо блокируется защитой сайта уже на втором запросе? Вроде бы и User‑Agent правильный, и прокси качественные, а сервер всё равно отвечает: Ты бот и сверху еще накидывает капчу.

Каждый раз, когда браузер открывает страницу, приложение обращается к API или очередной парсер собирает данные, почти всегда происходит один и тот же сценарий — устанавливается защищённое соединение по TLS. Мы привыкли воспринимать его как нечто само собой разумеющееся: браузер проверил сертификат, договорился с сервером о шифровании, и дальше можно спокойно передавать данные. Но есть один нюанс: шифрование содержимого трафика вовсе не означает, что клиент остается невидимым для сервера.

Ещё до того, как приложение отправит первый HTTP‑запрос, клиент успевает рассказать о себе достаточно много. Набор поддерживаемых шифров, порядок TLS‑расширений, параметры криптографических групп и даже особенности последующего общения по HTTP/2 формируют своеобразный цифровой почерк. По нему современные WAF и антифрод‑системы способны отличать обычный браузер от скрипта, консольного curl'а или самописного клиента на Go.

В этой статье разберемся, как устроено TLS‑рукопожатие, чем отличаются версии TLS 1.0 и 1.3 и почему даже переход на более современную криптографию не избавил клиентов от сетевого снифинга. Посмотрим, как работают алгоритмы JA3 и JA4, почему одного TLS‑отпечатка недостаточно и какую дополнительную информацию можно получить из HTTP/2. А затем перейдем к практике: разберемся, как управлять TLS‑отпечатком с помощью uTLS в Go и как использовать Finch для анализа и фильтрации входящего трафика.

Историческая справка: от амбиций Netscape до устройства современных шифров TLS

В середине 90-х интернет перестал быть игрушкой для академиков и начал стремительно превращаться в глобальный маркетплейс. Главной проблемой зарождающейся электронной коммерции была абсолютная прозрачность сети: данные кредитных карт и пароли летели по HTTP в открытом виде, доступные любому промежуточному маршрутизатору. Понимая, что без приватности бизнес не взлетит, инженеры компании Netscape под руководством Тахера Эльгамала в 1994 году сели проектировать решение. Их первая попытка, протокол SSL 1.0, содержала столько криптографических детских болезней (вроде уязвимостей к атакам повтора), что её даже не решились выпустить в паблик. Дебютировавший в 1995 году SSL 2.0 добрался до релиза в браузере Netscape Navigator 1.1, но тоже оказался дырявым как решето. Только к версии SSL 3.0 (1996 год) архитектуру удалось довести до ума. Позже консорциум IETF взял SSL 3.0 за основу, доработал хеш‑функции для генерации ключей и в 1999 году представил миру независимый открытый стандарт TLS 1.0 (технологически это был SSL 3.1). Так началась эпоха транспортной безопасности: именно эта эволюция заложила фундамент для криптографического рукопожатия, которое сегодня защищает триллионы терабайт мирового трафика.

Как эллиптические кривые спасли процессоры

В эпоху раннего TLS всем заправляла классическая асимметричная криптография RSA — именно она отвечала за проверку подлинности сервера и генерацию общих ключей. Чтобы защитить сессию, устройствам приходилось ворочать огромными простыми числами. Проблема обнаружилась в конце 2000-х с приходом эпохи смартфонов: мобильный интернет рос, а слабенькие процессоры первых гаджетов буквально плавились от необходимости высчитывать огромные 2048-битные ключи. В этот момент как раз и вышли эллиптические кривые (ECC). Их математика заключалась в том, что кривая вида y² = x³ + ax + b обеспечивала ту же стойкость, но при экстремально короткой длине ключа. Например, скромный 256-битный ключ популярной кривой X25519 давал защиту уровня RSA-3072. Переход на ECC в связке с алгоритмами класса ECDHE не только снизил нагрузку на сервера и мобильные чипы, но и подарил интернету прямую секретность.

Поскольку интернет состоит из миллионов абсолютно разных устройств — от древних банкоматов до ультрасовременных серверов — TLS проектировался, как гибкий конструктор. Чтобы клиент и сервер поняли друг друга, инженеры придумали наборы шифров. Это длинные строки‑инструкции, собиравшиеся как комбо‑обеды в KFC. Например, в шифре TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 прописывалось: за безопасное согласование первичного секрета отвечает ECDHE протокол на эллиптических кривых, подлинность самого сервера всё ещё заверяется старым добрым RSA, а трафик шифруется и проверяется на целостность с помощью AES‑GCM. А SHA256 работает как контролер, он генерирует общие ключи шифрования и защищает сам процесс подключения от вмешательства извне.

Эволюция рукопожатий: от TLS 1.0 к TLS 1.3

Чтобы понять, как современные антифрод‑системы вычисляют пользователей по первому сетевому пакету, нужно разобрать логику взаимодействия браузера и сервера на транспортном уровне L4 OSI. Каждое поколение протоколов пыталось решить две задачи: сделать соединение безопасным и снизить задержки.

В эпоху TLS 1.0–1.2 процесс рукопожатия требовал двух полных кругов обмена данными (2 RTT). Браузер отправлял пакет Client Hello со списком поддерживаемых версий, шифронаборов и расширений. Сервер выбирал комбинацию в Server Hello, прикладывал свой цифровой сертификат и отправлял параметры для генерации ключей. Только после второго круга согласования включалось шифрование.

Server Hello ответ с параметрами при использовании TLS 1.2
Server Hello ответ с параметрами при использовании TLS 1.2
Список поддерживаемых сервером криптографических шифров
Список поддерживаемых сервером криптографических шифров

У этой схемы было две проблемы. Первая — высокие задержки из‑за лишних сетевых запросов. Вторая — избыточная открытость: сертификат сервера, расширения браузера и методы сжатия передавались в чистом виде. Любой пассивный сниффер мог изучить параметры сессии до включения криптозащиты. Кроме того, отсутствие Perfect Forward Secrecy позволяло в будущем расшифровать исторический трафик при компрометации приватного ключа.

Стандарт TLS 1.3 (2018 год) сократил процедуру знакомства до одного круга (1 RTT) и убрал десятки устаревших шифров, оставив пять надежных вариантов на базе AEAD. Теперь браузер сразу отправляет в Client Hello заготовки ключей для популярных криптографических групп, а сервер принимает одну из них в ответном пакете. При повторном визите механизм сессионных тикетов (0-RTT) позволяет отправлять зашифрованный HTTP‑запрос сразу в первом пакете.

В качестве примера посмотрим на Client Hello при обращении к GitHub через Wireshark:

Весь байтовый пакет Client Hello TLS 1.3 с содержимым полей и расширений
Весь байтовый пакет Client Hello TLS 1.3 с содержимым полей и расширений
Список поддерживаемых клиентом криптографических шифров
Список поддерживаемых клиентом криптографических шифров
Расширение SNI используется для маршрутизации запросов к веб-серверу
Расширение SNI используется для маршрутизации запросов к веб‑серверу

В TLS 1.3 все сообщения сервера, включая сертификат сайта, шифруются сразу после получения заготовки ключа от клиента. Для внешнего наблюдателя открытым остается только первое приветствие Client Hello.

Поскольку Cloudflare, WAF и антифрод‑платформы лишились возможности анализировать сертификаты и последующие этапы хэндшейка, они сфокусировались на единственной видимой детали. Структура, порядок шифров и расширения в незашифрованном пакете Client Hello превратились в уникальный цифровой почерк, по которому сегодня и вычисляют ботов.

Цифровой почерк: анатомия TLS‑фингерпринтинга (JA3 и JA4)

Поскольку в TLS 1.3 все последующие этапы рукопожатия скрыты за криптографией, пакет Client Hello превратился в главный маркер идентификации. Идея TLS‑фингерпринтинга проста: каждый сетевой стек — будь то движок в Google Chrome, библиотека curl, Python‑пакет requests или кастомный троян на Go — формирует это первое сообщение по‑своему. Порядок шифронаборов, состав расширений и поддерживаемые форматы эллиптических кривых жестко зашиты в коде клиента. Для защитных систем это идеальный маркер. Зачем анализировать поведение бота, если его можно срезать на дальних подступах просто потому, что его сетевой стек заявляет себя как Chrome в HTTP‑заголовках, но здоровается в TLS как старый скрипт на Python?

Долгое время абсолютным стандартом в этой области оставался алгоритм JA3, разработанный инженерами Salesforce в 2017 году. Механика JA3 простая до гениальности: из сырого пакета Client Hello извлекаются строго пять параметров в определенном порядке — версия TLS, список поддерживаемых шифронаборов (Cipher Suites), список расширений (Extensions), поддерживаемые эллиптические кривые (Supported Groups) и форматы эллиптических кривых (Elliptic Curve). Значения внутри каждой категории записываются через запятую, а сами категории разделяются дефисом. Полученная текстовая строка прогоняется через хеш‑функцию MD5. Этот финальный 32-символьный хеш и есть отпечаток JA3. Если WAF видит запрос к эндпоинту авторизации с хешем, который гуглится как стандартный фингерпринт утилиты sqlmap, сессия мгновенно дропается.

[JA3 Fullstring: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,51764-16-65281-35-23-45-0-65037-13-5-11-43-51-27-10-17613-18-41,4588-29-23-24,0]
[JA3: 2a91540461ce04bac4c1c785ce9fa811]

Однако у JA3 со временем вскрылись серьезные архитектурные проблемы, которые для TLS 1.3 стали критическими. Первая беда — хрупкость: поскольку JA3 учитывает абсолютно все расширения и шифры в том порядке, в котором их выплюнул клиент, любое минимальное изменение (например, если браузер обновился и добавил одно новое расширение в конец списка) полностью меняет MD5-хеш. Вторая и главная проблема — инициатива Google по рандомизации TLS (GREASE).

Generate Random Extensions And Sustain Extensibility стандарт RFC 8701
Generate Random Extensions And Sustain Extensibility стандарт RFC 8701

Чтобы вендоры сетевого оборудования не затачивали свои парсеры под конкретные байты, современные браузеры при каждом новом рукопожатии намеренно подмешивают в Client Hello случайные мусорные шифры и расширения из специального диапазона. Для JA3 это стало катастрофой: один и тот же легитимный Chrome при пяти перезагрузках страницы генерировал пять абсолютно разных MD5-отпечатков, что сделало классические черные списки малоэффективными против умных клиентов. Если открыть Wireshark и запустить захват трафика, вы своими глазами увидите, как отпечатки JA3 скачут от сессии к сессии на одном и том же устройстве.

[JA3 Fullstring: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,51-13-10-65037-23-43-45-27-0-16-65281-35-5-11-51764-17613-18-41,4588-29-23-24,0]
[JA3: 7420fb81d80ea49cb26bf1b355881c06]

Чтобы окончательно понять масштаб катастрофы, достаточно взглянуть на реальные отпечатки Client Hello одного и того же браузера, собранные с разницей в несколько секунд. В первой сессии сырая строка параметров выглядит как 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,51-13-10-65037-23-43-45-27-0-16-65281-35-5-11-51764-17613-18-41,4588-29-23-24,0, что после хеширования дает строгий MD5-отпечаток 7420fb81d80ea49cb26bf1b355881c06. Но стоит пользователю просто обновить страницу, как тот же самый сетевой стек генерирует уже другую строку: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,51764-16-65281-35-23-45-0-65037-13-5-11-43-51-27-10-17613-18-41,4588-29-23-24,0. При неизменных шифрах и эллиптических кривых третья группа — список расширений — полностью перетасовалась, а на первое место вылезло число 51764. В шестнадцатеричной системе это 0xCAAC, типичный динамический мусор из зарезервированного пула GREASE. Из‑за этого минимального шума на выходе получается абсолютно новый хеш 2a91540461ce04bac4c1c785ce9fa811. Для любого классического WAF эти два запроса выглядят как посещения от двух совершенно незнакомых и никак не связанных между собой клиентов, что полностью обнуляет эффективность старых баз сигнатур.

Новый стандарт: JA4+

На смену устаревающему стандарту пришло семейство алгоритмов JA4+, анонсированное в 2023 году. JA4 кардинально меняет логику фингерпринтинга, делая отпечаток не просто слепым хешем, а легко читаемой человеком строкой, устойчивой к GREASE‑рандомизации. Базовый отпечаток клиента JA4 состоит из трех текстовых блоков, разделенных нижними подчеркиваниями, каждый из которых отвечает за свой уровень абстракции.

Первый блок, JA4_A, собирает мягкие свойства в виде 10-символьного кода. Первый символ обозначает тип транспорта (t для TCP, q для QUIC, d для DTLS), следующие два — версию TLS (например, 13). Затем идет один символ присутствия SNI (d — указан домен, i — указан IP/отсутствует). Далее фиксируются количество шифров (2 цифры), количество расширений (2 цифры) и два символа, взятые из первого и последнего знака протокола ALPN (например, h2 для HTTP/2 или 00, если ALPN нет). Важно, что из этого блока полностью исключены все GREASE‑значения, благодаря чему общая структура пакета остается стабильной при любых условиях.

Второй блок, JA4_B, отвечает за хеш шифров. Здесь берется список всех поддерживаемых Cipher Suites (опять же, с автоматическим вырезанием случайных GREASE‑байтов), сортируется по возрастанию номеров и хешируется по алгоритму SHA-256, от которого в финальный отпечаток попадают первые 12 символов. Сортировка — это решение проблемы, когда клиенты меняют порядок шифров ради оптимизации, намертво ломая старый JA3. Наконец, третий блок, JA4_C, обрабатывает список расширений: они так же сортируются по возрастанию без учета GREASE (за исключением SNI и ALPN), а затем к ним добавляются алгоритмы подписи в их исходном порядке. Полученная строка превращается во второй 12-символьный SHA-256 хеш.

[JA4: t13d1518h2_8daaf6152771_e2d80978ab2e]
[JA4_r: t13d1518h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,44cd,ca34,fe0d,ff01_0904,0905,0906,0403,0804,0401,0503,0805,0501,0806,0601]

Разберем реальный отпечаток выше и его сырой формат:

  • В блоке A (t13d1518h2) мы сразу считываем контекст соединения: клиент работает по протоколу TCP (t), использует TLS 1.3 (13), указал имя домена в SNI (d), предлагает ровно 15 шифров и 18 расширений (без учета GREASE), а также планирует общаться по протоколу HTTP/2 (h2).

  • В блоке B (8daaf6152771) скрывается SHA-256 хеш от списка шифров. Благодаря предварительной сортировке по возрастанию, этот хеш останется неизменным, даже если клиент намеренно перемешает их порядок.

  • В блоке C (e2d80978ab2e) зашифрован список TLS‑расширений, предварительно очищенный от SNI и ALPN и отсортированный по возрастанию, к которому в хвост приклеены алгоритмы подписи (0904,0905...0601) в их первоначальном, родном порядке.

В то время как JA3 легко обходится разработчиками ботов с помощью простой перетасовки расширений, меняющей итоговый MD5-хеш, протокол JA4 позволяет защитным системам действовать гораздо гибче. Даже если автоматизированный скрипт попытается перемешать расширения или подмешать случайные байты, блок JA4_A и предварительно отсортированные блоки B и C останутся идентичными оригинальному браузеру. Более того, системы анализа теперь могут блокировать клиентов избирательно: например, пропускать трафик, если блоки B и C валидны для Chrome, но дропать сессию, если блок JA4_A показывает, что клиент пытается работать по HTTP/1.1 там, где реальный браузер обязан использовать HTTP/2.

P.S если лень качать wireshark и настраивать его, то есть сайт для просмотра своего tls отпечатка браузера.

Там же можно тестировать свои реализации uTLS или скриптов на разных эндпоинтах:

Да, вставка мелкая, но всегда можно перейти по ссылке выше
Да, вставка мелкая, но всегда можно перейти по ссылке выше

Анатомия HTTP/2-фингерпринтинга

После успешного TLS‑рукопожатия общение клиента и сервера переходит на прикладной уровень. Для современных сайтов это практически всегда означает переход на протокол HTTP/2. На этом этапе защитные системы получают второй эшелон идентификации — h2-фингерпринтинг. Боту недостаточно просто сымитировать TLS‑приветствие браузера, ему нужно без ошибок воспроизвести механику его HTTP‑движка. В отличие от текстового HTTP/1.1, бинарный протокол HTTP/2 работает через фреймы. Сразу после открытия сессии клиент обязан отправить серверу стартовые пакеты конфигурации. Порядок этих фреймов, лимиты и приоритеты потоков зависят от конкретной сетевой библиотеки. Зачем злоумышленнику тратить колоссальные ресурсы на идеальную подделку TLS‑отпечатка JA4, если его скрипт выдаст себя на первом же HTTP/2-кадре, отправив настройки, которые реальный Google Chrome не использует уже несколько лет?

Долгое время стандартом для анализа оставался алгоритм HTTP2-fingerprint разработанный инженерами Akamai и Salesforce, который собирает хеш из четырех ключевых параметров бинарной сессии:

Akamai HTTP/2 fingerprint
MD5-хеш
52d84b11737d980aef856699f885ca86
Фрейм SETTINGS
1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
  1. SETTINGS_Frames (1:65536;2:0;4:6291456;6:262144): Список пар key:value из стартового фрейма настроек. Здесь клиент указывает свои лимиты: например, максимальное количество параллельных потоков или начальный размер окна отправки. Каждый параметр имеет свой ID, итоговая строка формируется как ID:значение, разделенные точкой с запятой. В данном примере клиент заявляет размер таблицы сжатия HPACK в 65 536 байт (1:65536), отключает Server Push (2:0), задает начальное окно потока в 6 МБ (4:6291456) и максимальный размер списка заголовков в 256 КБ (6:262144).

  2. WINDOW_UPDATE_Frame (15663105): Значение окна из фрейма управления потоком, который клиент отправляет следом, чтобы задать размер буфера для всего соединения (в примере — около 15 МБ).

  3. PRIORITY_Frames (0): Параметры приоритезации потоков трафика, где указываются ID зависимых потоков и их веса. Если фрейм отсутствует или механизмы приоритетов не задействованы в явном виде, ставится 0 или пустая строка.

  4. Pseudo‑Header Order (m,a,s,p): Порядок следования служебных псевдо‑заголовков HTTP/2. Буквенные маркеры заголовков записываются через запятую. Последовательность m,a,s,p расшифровывается по первым буквам: :method, :authority, :scheme, :path. Браузеры на движке Chromium всегда отправляют их именно в таком порядке, в то время как Firefox или кастомные сетевые библиотеки используют другие комбинации.

Полученная строка хешируется через MD5, формируя уникальный 32-символьный отпечаток сессии. Если WAF фиксирует легитимный TLS‑отпечаток под Windows‑браузер, но привязанный к нему HTTP/2-хеш совпадает со специфичным почерком консольной утилиты curl или библиотеки Hyper, запрос классифицируется как попытка спуфинга и блокируется.

Однако, классический h2-фингерпринтинг привязан исключительно к бинарным фреймам сессии, параметры которые браузеры регулярно оптимизируют, что плодит сущности в базах сигнатур. В ответ на это для детального анализа самих HTTP‑запросов был представлен модуль JA4H (JA4 HTTP).

Вместо фреймов, JA4H раскладывается на четкие блоки и хеширует контекст: метод запроса, версию протокола, точное количество текстовых заголовков (не считая Cookie и Referer), их алфавитный порядок и наличие специфических маркеров.

Комбинация TLS и HTTP/2-фингерпринтинга создает для WAF мощный двухуровневый фильтр: автоматизированным скриптам уже недостаточно просто подменить User‑Agent — им приходится полностью воссоздавать поведение реального браузера от криптографии TLS 1.3 до структуры бинарных фреймов. Однако для современных защитных систем уровня Cloudflare эти технические отпечатки (включая JA4) служат лишь базовым фундаментом. Анализируя колоссальные объемы глобального трафика, продвинутые фильтры обогащают фингерпринты поведенческим анализом на базе ИИ. Они в реальном времени оценивают репутацию IP, скорость запросов и сетевые задержки, поэтому даже идеально сэмулированный отпечаток будет мгновенно заблокирован, если его контекстное поведение не совпадает с паттернами живого пользователя.

Маскировка с помощью uTLS

Почему стандартный net/http выдаёт бота? Представим типичную ситуацию: мы пишем парсер на Go, добавляем в запрос заголовки User-Agent, Accept, Accept-Language и другие атрибуты, характерные для Google Chrome. На первый взгляд запрос выглядит вполне убедительно. Однако после отправки через стандартный net‑ или http‑сервер может мгновенно определить, что перед ним вовсе не браузер. Причина кроется в том, что HTTP‑заголовки и TLS‑отпечаток формируются на разных уровнях.

Заголовок User-Agent — это всего лишь строка, которую клиент передает серверу на уровне L7 HTTP. Она никак не влияет на то, какие шифронаборы, расширения и криптографические группы будут отправлены в приветственном сообщении Client Hello. Стандартная библиотека Go использует собственную реализацию TLS (crypto/tls), которая формирует рукопожатие в соответствии со своей внутренней логикой. В результате возникает критическое несоответствие: на уровне HTTP клиент притворяется Chrome, а на уровне TLS демонстрирует характерный отпечаток Go.

Управляем TLS‑рукопожатием

Библиотека uTLS — это форк и расширение реализации TLS для Go, основанное на crypto/tls, но предоставляющее низкоуровневый контроль над сообщением Client Hello. Её ключевая особенность — возможность использовать заранее подготовленные профили популярных клиентов, включая Chrome, Firefox и Safari. Вместо того чтобы вручную собирать каждый параметр рукопожатия, разработчик выбирает готовый пресет, который полностью имитирует структуру TLS‑приветствия нужного браузера. На практике это означает, что мы полностью заменяем стандартное поведение Go на логику Chrome ещё до отправки первого TLS‑пакета. Библиотека сформирует список шифронаборов и расширений в соответствии с выбранным пресетом.

Самая критическая ошибка при работе с TLS‑мимикрией — настроить пресет один раз и забыть о нем. Браузеры постоянно обновляются, а вместе с ними обновляются и их TLS‑отпечатки. Разработчики Chromium регулярно модифицируют свой криптографический стек: внедряют шифрование SNI (Encrypted Client Hello / ECH), делают обязательной поддержку постквантовых алгоритмов (таких как ML‑KEM / X25519MLKEM768), отказываются от устаревших шифронаборов или меняют логику работы TLS‑паддинга.

Отправляем запрос с профилем Chrome

Рассмотрим пример, в котором мы перехватываем создание TLS‑соединения и устанавливаем его самостоятельно с помощью uTLS:

Данный код приводится только в учебных целях и он далек от хороших реализаций
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"time"

	http "github.com/useflyent/fhttp"
	utls "github.com/refraction-networking/utls"
)

func main() {
	fmt.Println("=== Выберите эндпоинт для проверки отпечатка ===")
	fmt.Println("1 - All of the information (/api/all)")
	fmt.Println("2 - Only fingerprints      (/api/clean)")
	fmt.Println("3 - Only TLS data          (/api/tls)")
	fmt.Print("Введите цифру (1-3): ")

	var choice int
	_, err := fmt.Scanf("%d", &choice)
	if err != nil || choice < 1 || choice > 3 {
		log.Fatal("Некорректный ввод. Введите число от 1 до 3.")
	}

	var path string
	switch choice {
	case 1:
		path = "/api/all"
	case 2:
		path = "/api/clean"
	case 3:
		path = "/api/tls"
	}

	baseURL := "https://tls.peet.ws"
	fullURL := baseURL + path

	transport := &http.Transport{
		DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			host, _, err := net.SplitHostPort(addr)
			if err != nil {
				return nil, err
			}

			rawConn, err := (&net.Dialer{
				Timeout: 10 * time.Second,
			}).DialContext(ctx, network, addr)
			if err != nil {
				return nil, err
			}

			config := &utls.Config{
				ServerName: host,
				// Указываем h2 для ALPN-согласования на уровне TLS
				NextProtos: []string{"h2", "http/1.1"},
			}

			// Используем профиль автоматического подбора отпечатка Chrome
			conn := utls.UClient(rawConn, config, utls.HelloChrome_Auto)

			if err := conn.HandshakeContext(ctx); err != nil {
				conn.Close()
				return nil, err
			}

			return conn, nil
		},
		TLSHandshakeTimeout:   5 * time.Second,
		MaxIdleConns:          100,
		IdleConnTimeout:       10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		ForceAttemptHTTP2:     true, 
	}

	client := &http.Client{
		Transport: transport,
		Timeout:   25 * time.Second,
	}

	req, err := http.NewRequest(http.MethodGet, fullURL, nil)
	if err != nil {
		log.Fatalf("Ошибка при создании HTTP-запроса: %v", err)
	}

	req.Header = http.Header{
		"cache-control":             {"no-cache"},
		"upgrade-insecure-requests": {"1"},
		"user-agent":                {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
		"accept":                    {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"},
		"accept-language":           {"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7"},
		"priority":                  {"u=0, i"},
		
		// Задаем порядок обычных заголовков
		http.HeaderOrderKey: {
			"cache-control",
			"upgrade-insecure-requests",
			"user-agent",
			"accept",
			"accept-language",
			"priority",
		},
		
		// Задаем порядок псевдо-заголовков
		http.PHeaderOrderKey: {
			":method",
			":path",
			":authority",
			":scheme",
		},
	}

	fmt.Printf("\nОтправка запроса на %s...\n", fullURL)
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Не удалось выполнить запрос: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Не удалось прочитать тело ответа: %v", err)
	}

	fmt.Println("\n=== СЕТЕВАЯ ДИАГНОСТИКА ===")
	fmt.Println("Статус ответа сервера:", resp.Status)
	fmt.Println("Согласованный протокол:", resp.Proto)
	
	if strings.Contains(resp.Header.Get("Content-Type"), "application/json") {
		fmt.Println("\nДанные отпечатка от сервера (JSON):")
		fmt.Println(string(body))
	} else {
		fmt.Println("\nТело ответа (не JSON):")
		fmt.Println(string(body))
	}
}
Уникальный TLS‑fingerprint скрипта выше
=== Выберите эндпоинт для проверки отпечатка ===
1 - All of the information (/api/all)
2 - Only fingerprints      (/api/clean)
3 - Only TLS data          (/api/tls)
Введите цифру (1-3): 
Отправка запроса на https://tls.peet.ws/api/clean...

=== СЕТЕВАЯ ДИАГНОСТИКА ===
Статус ответа сервера: 200 OK
Согласованный протокол: HTTP/2

Данные отпечатка от сервера (JSON):
{
  "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,45-11-27-51764-13-51-65281-35-23-0-65037-10-16-18-17613-5-43-41,4588-29-23-24,0",
  "ja3_hash": "efa0c28162e6cccca786c71779268784",
  "ja4": "t13d1518h2_8daaf6152771_e2d80978ab2e",
  "ja4_r": "t13d1518h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,44cd,ca34,fe0d,ff01_0904,0905,0906,0403,0804,0401,0503,0805,0501,0806,0601",
  "akamai": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
  "akamai_hash": "52d84b11737d980aef856699f885ca86",
  "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|GREASE-2308-2309-2310-1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17613-18-23-27-35-41-43-45-5-51-51764-65037-65281-GREASE-GREASE",
  "peetprint_hash": "5fa343c29062ede7d0e28fd46c1052a7",
  "http_version": "h2"
}

Сначала открывается базовое TCP‑соединение, а библиотека uTLS маскирует его под отпечаток браузера Chrome с помощью профиля HelloChrome_Auto, безопасно выполняя TLS‑рукопожатие с сохранением проверки сертификатов и SNI.

Затем управление передаётся специализированной библиотеке fhttp, которая заменяет стандартный HTTP‑транспорт Go. Благодаря флагу ForceAttemptHTTP2, она автоматически переключает соединение на протокол HTTP/2 и позволяет вручную настроить точный порядок обычных заголовков и псевдо‑заголовков (:method, :path и так далее)

Как пример ещё приведу уникальный отпечаток curl для mac:

~ curl https://tls.peet.ws/api/clean 
{
  "ja3": "771,4867-4866-4865-52393-52392-52394-49200-49196-49192-49188-49172-49162-159-107-57-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49169-49159-5-4-49170-49160-22-10-255,43-51-0-11-10-13-16,29-23-24-25,0",
  "ja3_hash": "375c6162a492dfbf2795909110ce8424",
  "ja4": "t13d497h2_0d8feac7bc37_7395dae3b2f3",
  "ja4_r": "t13d497h2_0004,0005,000a,0016,002f,0033,0035,0039,003c,003d,0041,0045,0067,006b,0081,0084,0088,009c,009d,009e,009f,00ba,00be,00c0,00c4,00ff,1301,1302,1303,c007,c008,c009,c00a,c011,c012,c013,c014,c023,c024,c027,c028,c02b,c02c,c02f,c030,cca8,cca9,ccaa,ff85_000a,000b,000d,002b,0033_0806,0601,0603,0805,0501,0503,0804,0401,0403,0201,0203",
  "akamai": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
  "akamai_hash": "64a832f547be33249bf4d33e8a46c5dc",
  "peetprint": "772-771-770-769|2-1.1|29-23-24-25|2054-1537-1539-2053-1281-1283-2052-1025-1027-513-515|0||4867-4866-4865-52393-52392-52394-49200-49196-49192-49188-49172-49162-159-107-57-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49169-49159-5-4-49170-49160-22-10-255|0-10-11-13-16-43-51",
  "peetprint_hash": "0d6ceceb9e71ec096d2022ad0651e2f2",
  "http_version": "h2"
}

Собственный Client Hello

Если готовых пресетов недостаточно, uTLS позволяет полностью контролировать структуру запроса через режим utls.HelloCustom. Можно вручную сформировать список TLS‑расширений и передать его библиотеке. Это необходимо для тестирования совместимости, анализа сетевого стека или эмуляции редких версий клиентов.

Однако ручная сборка Client Hello требует глубокого понимания протокола. TLS‑расширения часто зависят друг от друга, а их параметры должны строго соответствовать общим настройкам криптографии. Случайная или некорректная комбинация элементов приведет к невалидному рукопожатию либо к аномалиям, которые легко детектируются сервером.

Finch: фильтруем трафик по TLS‑отпечаткам

До этого момента мы рассматривали фингерпринтинг преимущественно со стороны клиента. Но что, если нам нужно не просто посмотреть на JA3/JA4 в Wireshark, а использовать их для фильтрации входящего трафика? Для этого можно попробовать Finch — open‑source reverse proxy на Go от разрабочтика 0×4D31, который анализирует TLS‑рукопожатие и HTTP‑запросы, вычисляет JA3, JA4, JA4H и отпечатки HTTP/2, а затем принимает решения на основе заданных правил. По сути, это небольшой специализированный прокси с механизмом фильтрации по сетевым отпечаткам, а не полноценная замена крупным коммерческим WAF.

Допустим, у нас уже есть приложение, работающее на 8080, а Finch должен принимать HTTPS‑соединения на 8443 и проксировать запросы к приложению. Запустить его в упрощенном режиме можно следующей командой:

finch serve --listen :8443 --rules my.rules.hcl --upstream http://localhost:8080

Для начала можно вообще не блокировать трафик, а посмотреть, какие отпечатки приходят на сервер. Для этого у Finch предусмотрен отдельный режим echo, который запускает тестовый сервер и возвращает информацию о клиенте и вычисленных фингерпринтах вместо проксирования запросов:

finch echo --listen :8443

После запуска можно обратиться к /fp/, чтобы получить краткую JSON‑структуру с отпечатками, или к /fp/detail, чтобы посмотреть максимально подробные данные, включая параметры TLS ClientHello, заголовки и фреймы HTTP/2. Такой режим хорош для экспериментов: можно отправить запросы из Chrome, curl или кастомного скрипта, а затем сравнить, как их видит сервер.

Но самое интересное начинается, когда мы переходим от наблюдения к фильтрации. Finch поддерживает правила в формате HCL, которые позволяют гибко обрабатывать запросы по JA3, JA4, HTTP‑отпечаткам, сигнатурам Suricata и IP‑адресам. Например, следующее правило демонстрирует жесткую блокировку клиента с известным подозрительным JA3 сканера ZMap:

rule "block-scanner-ja3" {
  action = "deny"

  when {
    condition = tls.ja3 == "3518c438fe56bd7dba3f6e28example"
  }
}

Если TLS‑отпечаток совпадет с указанным в списке значением, Finch применит действие deny и мгновенно отклонит запрос.

Можно пойти дальше и учитывать сразу несколько признаков, комбинируя их. Например, правило может срабатывать, если совпал хотя бы один из подозрительных параметров трафика. Для этого используется блок when any:

rule "suspicious-client" {
  action = "tarpit"

  when {
    any = [
      tls.ja4 == "q13d0312h3_55b375c5d22e_c183556c78e2",
      http.ja4h == "bad-ja4h"
    ]
  }
}

Действие tarpit предназначено для намеренного замедления подозрительных клиентов по аналогии с tarpit HAProxy. Один и тот же JA4 может встречаться у множества легитимных пользователей, а TLS‑профиль браузера со временем меняется. Поэтому правильнее сначала собирать статистику, изучать реальные значения и только потом добавлять адресные правила для подтвержденных подозрительных клиентов.

У Finch есть так скажем нюанс: проект находится в активной разработке, а его текущая версия 0.1.0 прямо обозначена авторами, как экспериментальная и не готовая для полноценного релиза. Поэтому рассматривать его стоит прежде всего как интересный инструмент для исследований и изучения TLS‑фингерпринтинга.

Будущее протокола: ECH и его отпечаток

ECH (Encrypted Client Hello) — это утверждённый стандарт RFC 9849, который скрывает SNI и другие чувствительные расширения, например, ALPN от сторонних наблюдателей. При обычном TLS 1.3 домен передаётся открыто, чтобы сервер понимал, какой сайт нужно отобразить. ECH решает эту проблему: реальные параметры упаковываются в структуру ClientHelloInner и шифруются с помощью гибридного шифрования HPKE, а наружу передаётся стандартизированная заглушка ClientHelloOuter. Хотя стандарт механически допускает преднастройку ключей в клиенте, на практике для безопасного получения структуры ECHConfig необходим зашифрованный DNS (DoH/DoT/DoQ) — иначе запрос утечёт ещё на этапе DNS. У меня как раз недавно вышла статья где в Nginx добавили поддержку ECH и как это все можно настроить.

Для промежуточного сетевого фингерпринтинга на транзитных узлах это означает, что внешние наблюдатели больше не могут собирать корректные TLS‑отпечатки вроде JA3 или JA4. Во‑первых, они видят лишь стандартизированную внешнюю заглушку. Во‑вторых, даже если клиент не использует ECH, стандарт обязывает его применять механизм GREASE ECH (отправку пустых случайных ECH‑полей), благодаря чему реальные ECH‑сессии визуально не выделяются в общем потоке трафика и защищены от выборочных блокировок.

Однако, ECH не скрывает анализ трафика полностью. Защитные системы вроде WAF внутри инфраструктуры по‑прежнему видят уже расшифрованные внутренние параметры рукопожатия. Внешние же провайдеры всё ещё могут анализировать IP‑адреса, размеры пакетов, тайминги, а также поведение протоколов HTTP/2 и HTTP/3 с их залповыми TCP/UDP‑соединениями, которые у скриптов‑ботов и реальных браузеров сильно различаются. В итоге современные системы обнаружения ботов окончательно переходят от простых статических TLS‑хешей к комплексному поведенческому анализу всей совокупности действий пользователя.

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


  1. shuchkin
    27.10.2026 17:43

    приходят боты которые крутятся в headless-браузере, там эмулируется всё: протоколы, js, поведение, движения мыши, ии разгадывает капчи
    по логам интуитивно понимаю, что это бот, а объяснить не могу


    1. kinoz
      27.10.2026 17:43

      А ты выложи сюда логи, посмотрим. Но, как правило, детект делают уже на уровне приложения. Фингерпринт соединения спасает разве что от совсем тупых ботов. Плюс не надо забывать, что люди до сих пор сидят на Windows 7 с древними браузерами, где всё давно устарело. Так что смотреть надо не только на хеш соединения, но и на поведение на уровне приложения.


    1. leshoi Автор
      27.10.2026 17:43

      Присылай часть логов которые считаешь подозрительными, попробуем разобраться какой это ботнет


      1. shuchkin
        27.10.2026 17:43

        это биллинг, по безопасности не могу логи выкладывать,
        бот натасканный на форму регистрации, проходит проверку email и sms(!)


    1. rodial
      27.10.2026 17:43

      Я попробовал на днях antigravity от Гугла, там есть интеграция с хром. Он через эту интеграцию не просто headless использует, а открывает отдельную вкладку в уже запущенном хроме, делает скриншоты, двигает мышкой. Такое детектить только на самой странице.


  1. tuxi
    27.10.2026 17:43

    Надо будет попробовать Finch прикрутить и через /mirror с nginx на него запросы копировать, по виду прод решение вырисовывается


    1. leshoi Автор
      27.10.2026 17:43

      Если это сильно продвинутый ботнет, то помочь смогут только коммерческие waf, но опять же - есть бюджет на это или нет, можно ли прятаться за ним и тд. Нюансов слишком много, но защита там хорошая. Cloudflare заблокирован давно хотя они сейчас топ 1 по защите от dos атак и ботнета


      1. tuxi
        27.10.2026 17:43

        Ну пока имеем "то что имеем". Я попробую на одном проекте. Когда-то давно пробовал аналогичное, но из-за mirror получил исчерпание файловых дескрипторов (или что-то в этом духе) в течении 2 суток и эксперимент прекратил.