← Назад в базу знаний

TLS-фингерпринтинг JA4: как сайты различают прокси и реальный браузер на одном IP

TLS-фингерпринтинг JA4: как сайты различают прокси и реальный браузер на одном IP

Что вообще считает JA4

JA4 — это хеш от параметров TLS ClientHello. Не от IP, не от User-Agent. От того, что клиент отправил в первом пакете рукопожатия: набор шифров, расширения, группы эллиптических кривых, ALPN, порядок всего этого. FoxIO выпустили спецификацию в 2023 как замену JA3, которая ломалась от любого чиха — любая рандомизация GREASE превращала один и тот же Chrome в десять разных хешей.

JA3 брал SHA1 от строки вида `771,4865-4866-4867,0-11-10,29-23-24,0`. Проблема: Chrome с 2016 года вставляет GREASE-значения (0x0a0a, 0x1a1a и прочие) в случайные позиции. Хеш менялся при каждом соединении. Аналитики матерились и добавляли фильтры.

JA4 устроен иначе. Формат: `a_b_c`. Первая часть — `t` для TCP или `q` для QUIC, версия TLS, наличие SNI, количество шифров, количество расширений, ALPN. Вторая — отсортированный список шифров и расширений через запятую, SHA256 от них, обрезанный до 12 символов. Третья — сигнатура, где GREASE вычищен, а порядок сохранён.

```

t13d1516h2_8daaf6152771_b186095e22b6

```

Расшифровка: TCP, TLS 1.3, domain (SNI есть), 15 шифров, 16 расширений, HTTP/2 в ALPN. Дальше два хеша.

Ключевое: JA4 стабилен между сессиями одного и того же браузера. Chrome 120 на Windows и Chrome 120 на macOS дают одинаковый JA4, потому что BoringSSL собирается с одним набором параметров. Firefox — другой JA4, Safari — третий. Curl — четвёртый. Python requests — пятый.

Почему IP перестал быть сигналом

Лет десять назад антифрод жил на IP-репутации. Один IP — один пользователь, датацентровые подсети в чёрных списках, residential — в белых. Потом появились мобильные операторы с CGNAT, где за одним IPv4 сидят тысячи абонентов. Потом облачные провайдеры начали продавать residential-прокси через SDK, встроенные в бесплатные VPN. IP перестал что-либо значить.

Сейчас сигналов три: IP, поведение, TLS-фингерпринт. Первый дёшево подделать (прокси), второй дорого (эмуляция мыши, тайминги), третий вообще нельзя подделать без правки TLS-стека. Именно поэтому Cloudflare, Akamai, DataDome и PerimeterX начали собирать JA4 в промышленных масштабах с 2023-2024.

Логика простая. Если с одного IP приходят запросы с JA4 от Chrome, Firefox и curl вперемешку — это прокси-пул. Если JA4 всегда curl, но User-Agent говорит Chrome — это бот. Если JA4 Chrome, но версия в хеше старая (Chrome 108, а UA заявляет 122) — это автоматизация на устаревшем headless.

Как выглядит детект на практике

Сервер принимает ClientHello, считает JA4, пишет в лог вместе с IP, UA, Accept-Language, порядком заголовков (это уже HTTP/2 fingerprint, отдельная история — Akamai h2 fingerprint). Дальше правила.

Простое правило: JA4 не в белом списке браузеров. Бан.

Сложнее: JA4 в белом списке, но не совпадает с UA. Chrome UA + Firefox JA4 = автоматизация.

Ещё сложнее: JA4 коррелирует с IP. Residential-прокси пул из 500 IP, все с JA4 curl-8.5.0. Первый запрос проходит, второй через минуту с другого IP — тот же JA4, тот же порядок заголовков, тот же Accept-Encoding. Паттерн виден.

Вот как это выглядит в nginx с модулем, который логирует JA4:

```nginx

log_format ja4 '\$remote_addr - \$remote_user [\$time_local] '

'"\$request" \$status \$body_bytes_sent '

'"\$http_user_agent" ja4=\$ssl_ja4 '

'alpn=\$ssl_alpn sni=\$ssl_server_name';

server {

listen 443 ssl;

ssl_certificate /etc/ssl/cert.pem;

ssl_certificate_key /etc/ssl/key.pem;

ssl_protocols TLSv1.2 TLSv1.3;

access_log /var/log/nginx/ja4.log ja4;

}

```

Модуль `ssl_ja4` не в стандартной сборке. Есть патчи от Cloudflare и сторонние сборки. Альтернатива — терминировать TLS на HAProxy с Lua-хуком или на envoy с WASM-фильтром.

Python: считаем JA4 сами

Без серверной инфраструктуры JA4 можно считать на клиенте, чтобы понять, что о вас знают. Библиотека `ja4` от FoxIO есть на PyPI.

```python

import socket

import ssl

from ja4 import JA4

def grab_ja4(host, port=443):

ctx = ssl.create_default_context()

ctx.check_hostname = False

ctx.verify_mode = ssl.CERT_NONE

with socket.create_connection((host, port), timeout=10) as sock:

with ctx.wrap_socket(sock, server_hostname=host) as ssock:

binary = ssock.getpeercert(binary_form=True)

cipher = ssock.cipher()

version = ssock.version()

return {

"cipher": cipher[0],

"tls_version": version,

"ja4": JA4.from_socket(ssock),

}

print(grab_ja4("example.com"))

```

Модуль `JA4.from_socket` перехватывает ClientHello через monkey-patch ssl-контекста. Работает на Python 3.10+. Для requests-стека нужно патчить `urllib3` отдельно, потому что там свой SSLContext.

Реальный вывод для разных клиентов на одном хосте:

```

curl 8.5.0 -> t13d1516h2_8daaf6152771_02713d6af862

python 3.12 -> t13d1517h2_8daaf6152771_b0da82dd1658

chrome 122 -> t13d1516h2_8daaf6152771_b186095e22b6

firefox 123 -> t13d1715h2_5b57614c22b0_3d5424432f57

```

Первые две части у curl и Chrome совпадают — оба используют BoringSSL-совместимые наборы. Разница в третьей части, сигнатуре расширений. Firefox уникален: 17 шифров вместо 15, порядок другой.

Где ломается подмена

Идея «поставлю UA Chrome и всё пройдёт» умерла году в 2020. Сейчас нужно совпадение по четырём слоям: JA4, HTTP/2 fingerprint, порядок заголовков, тайминги.

HTTP/2 fingerprint (он же Akamai fingerprint) — это SETTINGS-фрейм, WINDOW_UPDATE, приоритеты, порядок псевдозаголовков. Chrome отправляет `:method :authority :scheme :path`, порядок фиксирован. Python httpx отправляет иначе. curl — третьим способом.

Порядок заголовков в HTTP/1.1 и HTTP/2 тоже сигнал. Chrome: `Host, Connection, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Upgrade-Insecure-Requests, User-Agent, Accept, Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-User, Sec-Fetch-Dest, Accept-Encoding, Accept-Language`. Любое отклонение — палево.

Тайминги. Chrome делает TLS handshake за 30-80 мс до первого байта HTTP. Python requests с холодным DNS — 200-400 мс. Если JA4 говорит Chrome, а задержка 350 мс — это не Chrome.

Кейс: маркетплейс и residential-прокси

Парсер маркетплейса крутился через пул из 2000 residential-прокси. UA ротировался, cookies чистились, задержки рандомизировались. Через три недели — бан 80% пула.

Причина: все запросы шли через Python requests 2.31 с OpenSSL 3.0. JA4 был идентичен для всех 2000 IP: `t13d1517h2_8daaf6152771_b0da82dd1658`. Антифрод собрал статистику — с 2000 разных residential IP приходит один и тот же TLS-фингерпринт, которого нет ни у одного реального браузера. Классический паттерн ботнета.

Решение: перешли на curl_cffi с импersonation Chrome 120. curl_cffi подменяет не только UA, но и TLS-стек через BoringSSL с нужными параметрами, и HTTP/2 fingerprint. JA4 стал `t13d1516h2_8daaf6152771_b186095e22b6` — точная копия Chrome. Плюс рандомизация порядка заголовков в пределах допустимого и задержки 80-250 мс. Бан упал до 5%.

Кейс: банк и headless Chrome

Банк ловил автоматизацию на логине. Стоял headless Chrome 118 через Puppeteer. JA4 совпадал с обычным Chrome 118 — BoringSSL один и тот же. Но детект срабатывал.

Причина оказалась в HTTP/2. Headless Chrome через Puppeteer отправляет WINDOW_UPDATE с другим значением, чем обычный Chrome, из-за отличий в инициализации сетевого стека при `--headless=new`. Akamai fingerprint не совпадал. Плюс отсутствовал `sec-ch-ua-full-version-list` — в headless он пустой.

Решение: запуск через `--headless=new` с флагом `--enable-features=NetworkServiceInProcess` и патч CDP-хука для подстановки `sec-ch-ua-full-version-list`. Плюс использование реального профиля Chrome с историей. Детект ушёл.

Кейс: CDN и легитимные прокси

Корпоративный клиент ходил через forward-прокси Squid. CDN начал отдавать капчу на каждый второй запрос.

Причина: Squid терминировал TLS и переоткрывал своё соединение к CDN с JA4 `t12d1509h2_...` — TLS 1.2, 9 шифров, старый OpenSSL. Для CDN это выглядело как автоматизация 2018 года. Реальные пользователи за прокси шли с Chrome JA4, а тут — Squid.

Решение: включили TLS bump с сохранением ClientHello (Squid 6.0+ умеет `ssl_bump peek` без модификации), либо перешли на CONNECT-туннелирование без терминции. JA4 стал клиентским, капча ушла.

IPv6 и JA4

Тут отдельная история. IPv6-адресов у residential-провайдеров в избытке — /64 на абонента, /48 на домохозяйство. Прокси-пулы на IPv6 выглядят чище: нет CGNAT-скученности, нет shared-репутации. Но JA4 всё равно палит.

Схема: 10 000 IPv6 из /48, все с JA4 curl. Антифрод видит 10 000 уникальных адресов с идентичным TLS-отпечатком. Это не 10 000 пользователей, это один скрипт. IPv6 не спасает, если фингерпринт один.

Обратная ситуация: реальные пользователи на IPv6 через мобильных операторов. У них JA4 Chrome/Safari, всё нормально. Проблема только у тех, кто гонит автоматизацию через IPv6-пулы без подмены TLS.

Прокси-сервисы с поддержкой IPv6 (включая lexic.ml, который работает с IPv6 с 2015) дают адресное пространство, но не решают проблему фингерпринта. Это разные слои. IPv6 — про сетевую идентичность, JA4 — про стек.

Как защищаться, если вы легитимны

Если вы не бот, а ваш трафик блокируют — проверьте JA4 своего клиента. Скорее всего, он палится.

Первое: обновите TLS-стек. OpenSSL 1.1.1 даёт один JA4, OpenSSL 3.0 — другой, BoringSSL — третий. Старые версии попадают в чёрные списки.

Второе: используйте библиотеки с импersonation. Python: `curl_cffi`, `tls-client`. Node: `puppeteer-extra-plugin-stealth` с патчами TLS. Go: `utls` с профилями Chrome/Firefox.

Третье: не миксуйте клиенты. Если приложение ходит и через requests, и через aiohttp, и через httpx — у каждого свой JA4. Антифрод увидит три разных отпечатка с одного IP и забанят все три.

Четвёртое: проверяйте HTTP/2 fingerprint. JA4 совпадает, а SETTINGS-фрейм отличается — детект. Инструмент: `h2fingerprint` от Salesforce, или онлайн-чекеры вроде tls.peet.ws.

Инструменты для проверки

`tls.peet.ws/api/all` — показывает JA3, JA4, Akamai fingerprint, порядок заголовков. Один GET, всё в JSON.

```bash

curl -s https://tls.peet.ws/api/all | jq '{

ja3: .tls.ja3_hash,

ja4: .tls.ja4,

akamai: .http2.akamai_fingerprint,

user_agent: .http_user_agent

}'

```

`browserleaks.com/tls` — визуально, для отладки.

Wireshark с диссектором JA4 — для разбора на уровне пакетов. Плагин есть в репозитории FoxIO.

Для массовой проверки пула — скрипт на Python, который гоняет `curl_cffi` через каждый прокси и сравнивает JA4 с эталоном Chrome. Расхождение — прокси или библиотека настроены неверно.

Что дальше

JA4 обновляется. FoxIO выпустили JA4+ — набор из шести фингерпринтов: JA4 (TLS), JA4S (server side), JA4H (HTTP), JA4X (X.509), JA4SSH (SSH), JA4D (DHCP). Каждый ловит свой слой. Антифрод постепенно переходит на комбинацию JA4 + JA4H + Akamai.

Гонка продолжается. Чем точнее детект, тем точнее импersonation. Библиотеки типа curl_cffi обновляются под каждую новую версию Chrome в течение недель. Антифрод-вендоры добавляют новые сигналы. Легитимным пользователям за прокси приходится держать стек в актуальном состоянии, иначе их трафик выглядит как бот.

✔️Купить прокси