TLS-фингерпринтинг: как сервер узнаёт твой клиент (JA3/JA4, uTLS)
TLS-фингерпринтинг: как сервер узнаёт твой клиент (JA3/JA4, uTLS)
Вообще я микроразработчик, со мной можно связаться как угодно, я открыт ко всему
Сторонний мой проект: Zapret2UI.
Все привыкли, что HTTPS - это про «никто не видит, что я делаю». Но есть штука, которая происходит до того, как включится шифрование, и она сдаёт про вас больше, чем кажется. Первый же пакет TLS-рукопожатия уходит открытым текстом, и по нему сервер (а заодно и любой, кто смотрит трафик по пути) с хорошей точностью говорит: это Chrome 133 на Windows. Или: это Firefox. Или: а вот это вообще не браузер, это чья-то программа на Go.
При этом User-Agent тут вообще ни при чём - его можно написать любой, отпечаток от этого не поменяется. И наоборот: подделали отпечаток, а User-Agent забыли - получили нестыковку, по которой вас палят ещё вернее, чем без всякой подделки.
Разберём, как это устроено технически, зачем оно нужно антифроду и антиботам, и - отдельным большим куском - зачем всё это знать именно вам, если вы сидите из России. Потому что там оно всплывает буквально в двух местах сразу: и когда ваш прокси-клиент палится по отпечатку, и когда вас ни с того ни с сего выкидывает из банковского приложения.
Кому пригодится: если гоняете Xray/VLESS и не понимаете, что за поле fingerprint в конфиге и почему от него что-то зависит; если ловите непонятные баны и капчи там, где их быть не должно; или если просто интересно, по каким мелочам вас на самом деле опознают в сети.
Тема дуальная по своей природе: одни и те же методы работают и на защиту (антибот, антифрод, поиск малвари), и на приватность. Разбираю механику, а не «как кого-то обмануть».
Содержание
- Что улетает открытым текстом до шифрования
- JA3: первая попытка сосчитать отпечаток
- Почему JA3 сломался
- JA4: тот же смысл, но человекочитаемо
- Семейство JA4 плюс: TLS тут лишь один слой
- Кто и зачем этим пользуется
- uTLS: как клиент подделывает отпечаток
- Зачем это знать, если вы из России
- Разбор поля fingerprint в конфиге Xray
- Грабли, на которые тут наступают
- Постквантовый ClientHello и почему он теперь в двух пакетах
- Как посмотреть свой отпечаток
- Короткий итог
Что улетает открытым текстом до шифрования
Начнём с базы, иначе дальше будет каша.
Когда клиент устанавливает TLS-соединение, первым делом он шлёт пакет ClientHello. Шифрования ещё нет - его же и договариваются установить, - поэтому пакет идёт открытым текстом. Внутри лежит примерно такое:
- версия TLS, которую клиент готов тянуть;
- список поддерживаемых шифров (cipher suites) - в том порядке, в каком клиент их предпочитает;
- список расширений (extensions) - тоже в своём порядке;
- поддерживаемые эллиптические кривые и их форматы;
- ALPN - какой протокол поверх пойдёт (
h2,http/1.1); - SNI - к какому домену вообще идём (про это была отдельная статья).
И вот тут начинается самое главное: этот набор у каждой TLS-библиотеки свой. Chrome собирает свой список шифров в своём порядке, Firefox - в другом, Safari - в третьем, а библиотека crypto/tls из стандартной поставки Go - в четвёртом, совсем не похожем ни на один браузер. Никто это специально не задумывал как опознавательный знак, просто так вышло: разные команды писали разный код с разными приоритетами.
Отсюда простая идея: возьмём эти поля, сложим в одну строку по фиксированным правилам, посчитаем хеш - и получим короткий идентификатор, по которому клиента можно узнавать. Вот с этого всё и началось.
JA3: первая попытка сосчитать отпечаток
JA3 придумали в Salesforce в 2017 году, и работает он ровно так, как я описал выше. Берутся пять полей из ClientHello, строго в этом порядке:
TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
Все значения переводятся в десятичные числа, поля разделяются запятой, значения внутри поля - дефисом. Получается длинная строка вида 771,4865-4866-4867-49195...,0-23-65281-10-11...,29-23-24,0. От неё считают MD5, и вот эти 32 шестнадцатеричных символа и есть JA3.
MD5 тут не ради криптографии (её тут никакой и нет), а чисто ради компактности - строка бывает длинной, а хеш всегда одного размера, его удобно хранить в базе и сравнивать.
Отдельно есть JA3S - то же самое, но для ответа сервера (ServerHello). Пара «JA3 клиента + JA3S сервера» уже говорит «кто с кем и как договорился», а не только «кто пришёл», и по такой паре ловили, например, малварь, которая ходит на свои командные серверы.
Одну важную мелочь в JA3 предусмотрели с самого начала: GREASE-значения выбрасываются. GREASE - это специально придуманный механизм, когда клиент подмешивает в списки заведомо мусорные значения, чтобы серверы не окостеневали и умели переваривать незнакомое. Если бы их считали как есть, отпечаток менялся бы при каждом соединении и был бы бесполезен.
Почему JA3 сломался
А теперь самое интересное в этой истории. В начале 2023 года Google в Chromium включил перемешивание порядка расширений в ClientHello - с каждым запуском порядок разный.
Задумано это было не против JA3, а против того самого окостенения: слишком много железок в интернете начали полагаться на конкретный порядок и ломались, если он менялся. Google решил проблему радикально - раз все привыкли к фиксированному порядку, пусть его не будет вообще.
Побочный эффект оказался нокаутирующим. JA3 считает расширения в том порядке, в каком они лежат - а порядок теперь случайный. Значит, у одного и того же Chrome на одной и той же машине JA3 разный от сессии к сессии. Как идентификатор он превратился в тыкву практически за одно обновление браузера.
Забавно, кстати, что это ровно та же логика, по которой Chrome в своё время добил и другие способы себя опознавать. Не со зла, просто «пусть на нас ничего не завязывают».
JA4: тот же смысл, но человекочитаемо
На замену пришёл JA4 от FoxIO, и там учли и рандомизацию, и опыт предыдущих лет. Два принципиальных отличия:
- Расширения и шифры сортируются перед хешированием. Chrome может тасовать их как хочет - после сортировки результат один и тот же.
- Отпечаток частично читается глазами, вместо одного непрозрачного хеша.
Выглядит он так:
t13d1516h2_8daaf6152771_e5627efa2ab1
Разбираем по кускам - и вот это, на мой взгляд, реально красиво сделано:
| Кусок | Что значит |
|---|---|
t | Транспорт: t - TLS поверх TCP, q - QUIC, d - DTLS |
13 | Версия TLS - тут 1.3 |
d | SNI есть, идём по доменному имени (i было бы «идём на голый IP») |
15 | Количество шифров - 15 штук |
16 | Количество расширений - 16 штук |
h2 | Первый и последний символ первого значения ALPN, тут h2 |
8daaf6152771 | Обрезанный SHA256 от отсортированного списка шифров |
e5627efa2ab1 | Обрезанный SHA256 от расширений плюс алгоритмы подписи |
То есть даже не зная хешей, по первым десяти символам уже видно: пришёл клиент по TCP, TLS 1.3, с доменом в SNI, с 15 шифрами, 16 расширениями и HTTP/2 сверху. Аналитик читает это как строчку в логе, а не как «случайные 32 символа, ищи по базе».
GREASE тут тоже выкидывается, как и в JA3.
Семейство JA4 плюс: TLS тут лишь один слой
JA4 - это не один отпечаток, а набор, который у FoxIO называется JA4+. Логика простая: TLS не единственное место, где клиент себя выдаёт своей манерой.
- JA4 - клиентский TLS, тот самый.
- JA4S - ответ сервера, наследник JA3S.
- JA4H - отпечаток на уровне HTTP: набор и порядок заголовков, куки, язык. Штука неприятная тем, что работает даже когда TLS-слой вы аккуратно подделали, - там своя манера, которую надо подделывать отдельно.
- JA4X - отпечаток по X.509-сертификату, помогает связывать инфраструктуру, выпущенную одними руками.
- JA4L, JA4SSH и прочие - по задержкам, по SSH-трафику и так далее.
Тенденция понятная: одного слоя мало, ловят по совокупности. Подделали TLS, но забыли про HTTP - вас всё равно видно, просто на этаж выше.
Кто и зачем этим пользуется
Тут стоит сказать прямо, что это в первую очередь оборонительная технология, а не инструмент слежки за вами лично:
- Антибот и антифрод. Самое массовое применение. Скрипт на Python с библиотекой
requestsимеет отпечаток, который не совпадает ни с одним браузером на планете. Никакой User-Agent «Mozilla/5.0...» его не спасёт - отпечаток считается ниже, на уровне самого TLS, и подделке из скрипта поддаётся тяжело. - Защита от парсинга. Cloudflare и подобные сервисы гоняют это в проде постоянно, и именно поэтому «просто подставить заголовки браузера» в парсере давно не работает.
- Поиск малвари в SOC. Троян, который стучится на командный сервер, обычно использует какую-то одну библиотеку - и палится стабильным отпечатком даже в зашифрованном трафике, содержимое расшифровывать не надо.
- Классификация трафика на сетевом оборудовании. А вот это уже ближе к нашей теме, к этому вернёмся отдельно.
uTLS: как клиент подделывает отпечаток
Раз отпечаток берётся из того, что клиент сам кладёт в ClientHello - значит, клиент может положить туда что-то другое. Проблема только в том, что обычные TLS-библиотеки такого не умеют: они собирают пакет так, как в них зашито, и снаружи это не поменять.
Отсюда родился uTLS - форк стандартной TLS-библиотеки Go от команды refraction-networking, который даёт низкоуровневый доступ к ClientHello. В нём заготовлены готовые слепки настоящих браузеров, и библиотека собирает пакет байт в байт как выбранный браузер: тот же порядок, те же шифры, те же расширения.
Слепков там прилично: Chrome разных версий вплоть до 133, Firefox, Safari, iOS, Edge, плюс китайские 360 и QQ. Библиотека живая, обновляется - последний релиз на момент написания статьи был в январе 2026-го, что важно, потому что браузеры меняются и слепки надо догонять.
Именно на uTLS и стоит вся история с маскировкой у современных прокси. Собственно, поле fingerprint в конфиге Xray - это ровно выбор слепка из этого списка, ничего больше.
Зачем это знать, если вы из России
Вот теперь к практике, потому что тут это всплывает не в теории, а в двух совершенно бытовых местах.
Первое: ваш прокси-клиент опознаётся по отпечатку. ClientHello, напомню, идёт открытым текстом, расшифровывать ничего не надо. Если ваш клиент ходит с отпечатком, который не похож ни на один живой браузер, - он выделяется на общем фоне просто по факту своей непохожести, безо всякого анализа содержимого. Вот ровно поэтому в конфигах и стоит fingerprint: "chrome", а не потому, что кто-то любит Chrome.
Второе, куда более бытовое: антифрод. Тут смешная штука получается. Вы сидите через прокси, который честно подделывает отпечаток Chrome, а браузер у вас Firefox и User-Agent говорит «Firefox». Для банка, маркетплейса или любой площадки с нормальным антифродом это рассинхрон: на уровне TLS пришёл Chrome, на уровне HTTP - Firefox. Такого сочетания в природе не бывает, и реакция предсказуемая - капча, дополнительная проверка, а то и блок сессии. Люди потом ходят и гадают, чего это их вдруг стало выкидывать.
Мораль отсюда простая и невесёлая: отпечаток должен быть согласован со всем остальным, а «поставил что-то, лишь бы не дефолт» - это не работа, это самообман. Одна подделанная деталь при десяти неподделанных делает вас заметнее, а не незаметнее.
Разбор поля fingerprint в конфиге Xray
Раз уж мы разбирали конфиги Xray построчно, давайте закроем это поле по-нормальному - оно того стоит, там несколько неочевидных вещей.
Что принимается (смотрел прямо в исходниках Xray, а не в чужих гайдах):
| Значение | Что реально происходит |
|---|---|
chrome, firefox, safari, ios, android, edge, 360, qq | Косплеим конкретный браузер, слепок берётся из uTLS. Обычный, вменяемый выбор |
| пусто (поля нет вообще) | Не «без маскировки», как многие думают, а тихо подставляется Chrome. uTLS всё равно включается |
random | Один слепок выбирается случайно при старте процесса и держится до перезапуска |
randomized | Собирается синтетический случайный ClientHello, который не соответствует никакому реальному браузеру |
| несуществующее значение (опечатка) | Зависит от security. На обычном TLS - молча падаем на родной стек Go, ошибки в логе нет, просто отпечаток теперь как у Go-программы. На Reality - жёсткая ошибка REALITY: failed to get fingerprint, соединение не встанет вообще |
Отдельно про random - тут прям массовое заблуждение, и я сам его в своё время повторял. Это не «новый отпечаток на каждое соединение». Xray выбирает один слепок из своего списка современных при инициализации и дальше живёт с ним, пока процесс не перезапустят. Смысл в том, чтобы разные пользователи выглядели по-разному, а не в том, чтобы мельтешить внутри одной сессии.
И ещё нюанс, который прямо следует из кода: в списке, откуда тянется random, лежат вперемешку свежие слепки и заметно пожилые - iOS 13, iOS 14, Edge 106. Выпадет вам такой - и вы косплеите браузер из позапрошлой эпохи, а это само по себе аномалия: живых клиентов с таким отпечатком в 2026 году почти не осталось. Вот отсюда и растёт мнение, что random не так хорош, как звучит.
Грабли, на которые тут наступают
- Ставят
randomized, думая, что это самый скрытный вариант. На деле наоборот: получается отпечаток, не совпадающий ни с одним реальным браузером, то есть уникальный. Спрятаться в толпе - это быть как все, а не быть неповторимым. - Опечатка в названии слепка.
chrome131вместоchrome- и вы молча уехали на голый Go-стек. Ошибки не будет, работать всё будет, а отпечаток при этом самый палевный из возможных. - Подделали TLS, забыли про всё остальное. JA4H на уровне HTTP, порядок заголовков, набор шрифтов в браузере, разрешение экрана - слоёв много, а согласовывать надо все.
- Взяли слепок и забыли на два года. Chrome обновляется каждые несколько недель, реальные отпечатки уезжают. Слепок Chrome 96 сегодня - это как прийти в костюме десятилетней давности: вроде костюм, а всё равно оборачиваются.
- Считают, что отпечаток шифруется. Не шифруется. ClientHello открытым текстом - в этом весь фокус.
Постквантовый ClientHello и почему он теперь в двух пакетах
Свежая история, про которую мало кто в курсе, а она уже поменяла картину.
Chrome с августа 2024 года по умолчанию включил постквантовый обмен ключами - гибрид X25519MLKEM768. Практическое следствие: доля с ключом в ClientHello раздулась до 1216 байт (32 байта от X25519 плюс 1184 от ML-KEM-768), и весь ClientHello перестал влезать в один TCP-сегмент. Теперь он штатно разъезжается на два пакета.
Отсюда два следствия, оба интересных:
- Отпечатки поехали. Слепки браузеров, снятые до постквантовой эпохи, перестали совпадать с живым Chrome - оттого в uTLS и появились отдельные слепки с пометкой
PQ. - Железки, которые не умеют собирать ClientHello из двух пакетов, начали ломаться. Всякий анализатор трафика, который привык, что рукопожатие приходит одним куском, столкнулся с новой реальностью. Тут прямая связка с фрагментацией, про которую я писал в статье про Xray: раньше разрыв ClientHello на куски был экзотикой, а теперь это штатное поведение самого популярного браузера планеты.
Ирония момента в том, что Google, добавляя постквантовую криптографию, попутно сделал раздельный ClientHello нормой - а норму уже нельзя считать признаком чего-то подозрительного.
Как посмотреть свой отпечаток
Проще всего просто открыть и посмотреть - это бесплатно и занимает секунд десять:
- browserleaks.com/tls - показывает ваши JA3 и JA4 по-человечески, плюс полный список шифров и расширений.
- tls.peet.ws - подробнее и ближе к железу: сырой разбор ClientHello, порядок расширений, отдельно отпечаток HTTP/2.
Полезное упражнение: откройте один и тот же сервис из обычного браузера и из-под своего клиента, а потом сравните. Сразу станет видно, что ваш конфиг реально показывает наружу, - а это куда убедительнее любых рассуждений о том, как оно должно работать в теории.
Короткий итог
- ClientHello идёт открытым текстом до всякого шифрования, и набор с порядком шифров и расширений выдаёт конкретную TLS-библиотеку.
- JA3 (Salesforce, 2017) - это MD5 от пяти полей ClientHello. Простой, массово внедрённый и уже почти нерабочий.
- Сломала его рандомизация порядка расширений в Chromium с начала 2023-го: отпечаток стал плавать от сессии к сессии.
- JA4 решает это сортировкой и заодно делает отпечаток частично читаемым глазами:
t13d1516h2_...разбирается без всякой базы. - JA4+ ловит по совокупности слоёв - TLS, HTTP, сертификаты, тайминги. Подделали один - остаются остальные.
- uTLS - библиотека, которая собирает ClientHello байт в байт как выбранный браузер. Поле
fingerprintв Xray - это ровно выбор слепка из неё. randomвыбирается один раз при старте процесса, а не на каждое соединение, и может выпасть на устаревший слепок.randomizedвообще не похож ни на один реальный браузер, что хуже, а не лучше.- Главное практическое правило: отпечаток должен быть согласован со всем остальным. Chrome на уровне TLS плюс Firefox в User-Agent - это не маскировка, а флаг.
Вообще я микроразработчик, со мной можно связаться как угодно, я открыт ко всему
Сторонний мой проект: Zapret2UI.
Источники
- JA4+ - репозиторий FoxIO с полной спецификацией
- JA4 - технические детали формата
- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S
- uTLS - репозиторий refraction-networking
- Stamus Networks: как рандомизация расширений добила JA3
- BrowserLeaks TLS - посмотреть свой отпечаток
- TrackMe (tls.peet.ws) - подробный разбор ClientHello

