Давайте дружить в Телеграме: рассказываем про новые фичи и общаемся в комментах Подписаться
support@serv.host
Личный кабинет

TLS-фингерпринтинг: как сервер узнаёт твой клиент (JA3/JA4, uTLS)

TLS-фингерпринтинг: как сервер узнаёт твой клиент (JA3/JA4, uTLS)

Вообще я микроразработчик, со мной можно связаться как угодно, я открыт ко всему

Сторонний мой проект: Zapret2UI.


Все привыкли, что HTTPS - это про «никто не видит, что я делаю». Но есть штука, которая происходит до того, как включится шифрование, и она сдаёт про вас больше, чем кажется. Первый же пакет TLS-рукопожатия уходит открытым текстом, и по нему сервер (а заодно и любой, кто смотрит трафик по пути) с хорошей точностью говорит: это Chrome 133 на Windows. Или: это Firefox. Или: а вот это вообще не браузер, это чья-то программа на Go.

При этом User-Agent тут вообще ни при чём - его можно написать любой, отпечаток от этого не поменяется. И наоборот: подделали отпечаток, а User-Agent забыли - получили нестыковку, по которой вас палят ещё вернее, чем без всякой подделки.

Разберём, как это устроено технически, зачем оно нужно антифроду и антиботам, и - отдельным большим куском - зачем всё это знать именно вам, если вы сидите из России. Потому что там оно всплывает буквально в двух местах сразу: и когда ваш прокси-клиент палится по отпечатку, и когда вас ни с того ни с сего выкидывает из банковского приложения.

Кому пригодится: если гоняете Xray/VLESS и не понимаете, что за поле fingerprint в конфиге и почему от него что-то зависит; если ловите непонятные баны и капчи там, где их быть не должно; или если просто интересно, по каким мелочам вас на самом деле опознают в сети.

Тема дуальная по своей природе: одни и те же методы работают и на защиту (антибот, антифрод, поиск малвари), и на приватность. Разбираю механику, а не «как кого-то обмануть».


Содержание

  1. Что улетает открытым текстом до шифрования
  2. JA3: первая попытка сосчитать отпечаток
  3. Почему JA3 сломался
  4. JA4: тот же смысл, но человекочитаемо
  5. Семейство JA4 плюс: TLS тут лишь один слой
  6. Кто и зачем этим пользуется
  7. uTLS: как клиент подделывает отпечаток
  8. Зачем это знать, если вы из России
  9. Разбор поля fingerprint в конфиге Xray
  10. Грабли, на которые тут наступают
  11. Постквантовый ClientHello и почему он теперь в двух пакетах
  12. Как посмотреть свой отпечаток
  13. Короткий итог

Источники


Что улетает открытым текстом до шифрования

Начнём с базы, иначе дальше будет каша.

Когда клиент устанавливает 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, и там учли и рандомизацию, и опыт предыдущих лет. Два принципиальных отличия:

  1. Расширения и шифры сортируются перед хешированием. Chrome может тасовать их как хочет - после сортировки результат один и тот же.
  2. Отпечаток частично читается глазами, вместо одного непрозрачного хеша.

Выглядит он так:

t13d1516h2_8daaf6152771_e5627efa2ab1

Разбираем по кускам - и вот это, на мой взгляд, реально красиво сделано:

КусокЧто значит
tТранспорт: t - TLS поверх TCP, q - QUIC, d - DTLS
13Версия TLS - тут 1.3
dSNI есть, идём по доменному имени (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.


Источники