TLS-фингерпринтинг JA4: как сайты различают прокси и реальный браузер на одном IP
Содержание
- Что вообще считает JA4
- Почему IP перестал быть сигналом
- Как выглядит детект на практике
- Python: считаем JA4 сами
- Где ломается подмена
- Кейс: маркетплейс и residential-прокси
- Кейс: банк и headless Chrome
- Кейс: CDN и легитимные прокси
- IPv6 и JA4
- Как защищаться, если вы легитимны
- Инструменты для проверки
- Что дальше
Что вообще считает 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 в течение недель. Антифрод-вендоры добавляют новые сигналы. Легитимным пользователям за прокси приходится держать стек в актуальном состоянии, иначе их трафик выглядит как бот.