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

Как антифрод вычисляет прокси по TLS-отпечатку JA3/JA4

Как антифрод вычисляет прокси по TLS-отпечатку JA3/JA4

Открываешь сайт через свежий резидентный прокси. IP чистый, геолокация совпадает, User-Agent правильный. А тебя всё равно режут на втором запросе. Знакомо? Причина часто в TLS-рукопожатии, которое ты не контролируешь.

Что вообще такое TLS-отпечаток

Когда клиент устанавливает HTTPS-соединение, первое сообщение — ClientHello. В нём куча полей: версия TLS, список поддерживаемых шифров (cipher suites), список расширений, эллиптические кривые, форматы точек, ALPN. Порядок и набор этих полей зависит от библиотеки, её версии, сборки, даже от флагов компиляции.

JA3 — это MD5-хеш от конкатенации пяти полей ClientHello: версия TLS, cipher suites, extensions, elliptic curves, elliptic curve point formats. Salesforce придумали его в 2017, и с тех пор он разошёлся по всем антифрод-платформам.

JA4 — переработка от FoxIO, вышла в 2023. Вместо одного MD5 — три части: `q` или `t` (QUIC/TCP), версия TLS, число шифров и расширений, ALPN, а затем уже хеши. Главное отличие: JA4 устойчив к рандомизации порядка расширений (GREASE), которая ломала JA3.

```bash

посмотреть свой JA3 через curl + tcpdump

sudo tcpdump -i eth0 -s 0 -w hello.pcap 'tcp port 443 and tcp[((tcp[12] & 0xf0) >> 2)] = 0x16'

tshark -r hello.pcap -T fields -e tls.handshake.ja3 -e tls.handshake.ja4

```

Запусти это на своей машине с обычным curl и с curl-impersonate. Увидишь два разных отпечатка.

Почему прокси тут ни при чём

Прокси меняет IP. Иногда — заголовки HTTP. Но TLS-рукопожатие устанавливает твой клиент, а не прокси. Если ты гонишь трафик через HTTP-прокси (CONNECT), TLS-сессия идёт end-to-end между твоим кодом и сервером. Отпечаток твой.

С SOCKS5 то же самое. Прокси видит только байты, шифрует и пересылает. ClientHello уходит как есть.

Исключение — MITM-прокси с подменой сертификата. Но тогда ты видишь чужой сертификат и обычно ломается проверка цепочки. Плюс такие прокси сами палятся по issuer.

Как выглядит типичный зоопарк отпечатков

Вот реальные JA3 из продакшена за один день логов на среднем e-commerce:

| Клиент | JA3 | Доля трафика |

|---|---|---|

| Chrome 120 Win | `cd08e31494f9531f560d64c695473da9` | 34% |

| Chrome 120 macOS | `cd08e31494f9531f560d64c695473da9` | 11% |

| Safari 17 iOS | `773906b0efdefa24a7f2b8eb6985bf37` | 18% |

| Firefox 121 | `579ccef312d18482fc42e2b822ca2430` | 9% |

| Python requests 2.31 | `3b5074b1b5d032e5620f69f9f700ff0e` | 0.4% |

| Go net/http 1.21 | `8faf27a11a8b9f4e9b9b5e2e8e5e8e5e` | 0.2% |

| curl 8.5 | `1d4b7e6c3f8b9e2a1c4d5e6f7a8b9c0d` | 0.1% |

Видишь закономерность? Реальные браузеры дают миллионы запросов, автоматизация — единицы. Если с одного IP прилетает 500 запросов с JA3 Python requests, а среди них ноль Chrome — картина маслом.

Chrome и Safari на разных ОС часто дают одинаковый JA3, потому что BoringSSL и Network.framework собираются предсказуемо. А вот Python requests тянет OpenSSL, и его отпечаток стабилен годами. Это и есть главный маркер.

Что смотрит антифрод кроме самого хеша

Хеш — только вход. Дальше идёт корреляция.

Первое: JA3 против User-Agent. Если UA говорит «Chrome 120», а JA3 — от OpenSSL, это красный флаг. Несоответствие ловится тривиально: держишь словарь «UA → ожидаемый JA3» и сверяешь.

Второе: JA4 против JA4H. JA4H — это отпечаток HTTP/2-фреймов: порядок псевдозаголовков, приоритеты, настройки SETTINGS-фрейма. Chrome шлёт `:method :authority :scheme :path`, а Python httpx может швырнуть иначе. Комбинация JA4 + JA4H + UA даёт почти уникальный идентификатор клиента.

Третье: поведение на уровне TCP. Window size, TTL, порядок опций (MSS, SACK, Timestamps), initial congestion window. Linux, Windows и macOS отличаются. Если у тебя JA3 Chrome, а TCP-стек Linux с дефолтным окном 29200 — снова несоответствие.

Четвёртое: частота и паттерн. Даже идеальный отпечаток Chrome не спасёт, если ты делаешь 200 запросов в минуту с одного IP без пауз, без загрузки картинок, без CSS. Антифрод смотрит на ratio: HTML-запросов против статики. У человека оно около 1:20, у парсера — 1:0.

Как ловят именно прокси-пулы

Прокси-пул — это не один IP, а сотни. Логика антифрода такая: собираешь (JA3, JA4H, TCP-fingerprint) со всех запросов за сутки. Если 300 разных IP дают одинаковый редкий отпечаток — это один клиент за прокси-ротацией.

Пример: сервис на nginx 1.24 с модулем ModSecurity и внешним fingerprint-сервисом. За 6 часов 4 200 запросов с 680 IP. Все с JA3 `3b5074b1b5d032e5620f69f9f700ff0e` (Python requests). Все с одинаковым порядком HTTP/2-заголовков. Все без запросов к `/static/`. Бан-лист вырос на 680 подсетей за минуту.

Вот тут как раз пригодится lexic.ml — если нужен пул IPv6, где каждый адрес выдаётся как отдельная /128 и не светится как диапазон, потому что IPv6-подсети /64 в антифроде палятся даже быстрее, чем IPv4.

Почему IPv6-прокси меняет расклад

С IPv4 всё понятно: /24 — это один блок, и репутация блока общая. Если один адрес из /24 засветился, весь блок может попасть в серые списки.

С IPv6 иначе. Провайдер выдаёт клиенту /56 или /48, внутри — /64 на каждую подсеть, и адресов там 2^64. Антифрод-системы обычно агрегируют репутацию до /64 или /48. Если ты ротируешь адреса внутри одного /64, они для антифрода — один и тот же клиент. Это грабли, на которые наступают все.

Правильная ротация IPv6 — это разные /64, а лучше разные /48. Тогда каждый запрос выглядит как отдельный клиент с точки зрения сетевой репутации. Но TLS-отпечаток всё равно остаётся твоим. Так что IPv6-пул решает проблему IP-репутации, но не проблему fingerprinting.

Практика: подмена JA3 через curl-impersonate

Стандартный curl бесполезен. Нужен curl-impersonate или его Python-обёртка curl_cffi.

```python

from curl_cffi import requests

эмулируем Chrome 120

r = requests.get(

"https://example.com/api/products",

impersonate="chrome120",

proxies={

"https": "http://user:pass@[2a01:4f8:1c1c:abcd::1]:8080"

},

timeout=15

)

print(r.status_code, r.json()["count"])

```

curl_cffi подменяет не только заголовки, но и порядок TLS-расширений, набор шифров, HTTP/2-фреймы. JA3 становится идентичен Chrome. JA4H тоже.

Но есть нюанс: curl_cffi эмулирует Chrome на Linux, а не на Windows. TCP-отпечаток остаётся линуксовым. Если антифрод сверяет TCP-опции с заявленной ОС — снова несоответствие. Полное совпадение даёт только запуск реального Chrome через Playwright с прокси на уровне браузера.

Как проверить, палишься ли ты

Собери baseline. Запусти свой скрейпер и параллельно поснифай трафик. Сравни JA3 с эталонным Chrome.

```bash

эталонный JA3 Chrome 120

curl-impersonate-chrome -s https://tls.browserleaks.com/json | jq .ja3_hash

твой скрейпер

python scraper.py --url https://tls.browserleaks.com/json

```

Если хеши разные — ты палишься на уровне TLS. Если одинаковые, но тебя всё равно банят — смотри JA4H, TCP и поведенческие метрики.

Ещё один быстрый тест: `https://tls.peet.ws/api/all`. Он показывает JA3, JA4, Akamai fingerprint (HTTP/2), порядок заголовков. Гоняй через него каждую конфигурацию прокси + клиента.

Кейс: парсер маркетплейса на 200k страниц

Задача: собрать цены с крупного маркетплейса. Стек: Python requests + резидентные IPv4-прокси, ротация каждые 10 запросов.

Первые 3 000 запросов прошли. На 3 001 — 403 с Cloudflare-страницей. Причина: все запросы имели JA3 Python requests, а Cloudflare на этом эндпоинте включил JA3-фильтр для sensitive-путей. IP-ротация не помогала, потому что фингерпринт не менялся.

Решение: переписали на curl_cffi с `impersonate="chrome120"`. Плюс добавили заголовки `sec-ch-ua`, `sec-fetch-*`, `accept-language` в правильном порядке. Скорость упала с 40 req/s до 12 req/s, но 403 исчезли. За 18 часов собрали 198k страниц, 412 ошибок (0.2%) — в основном таймауты.

Кейс: несовпадение JA3 и TCP в AWS Lambda

Другой сценарий. Скрейпер крутится в Lambda, исходящий трафик идёт через NAT Gateway. IP-адрес AWS, регион us-east-1. Клиент — Node.js с axios.

Антифрод банка видел: JA3 от Node.js (OpenSSL), TCP-отпечаток Linux, IP из AWS-диапазона, но геолокация в заголовках — Германия. Три несоответствия. Бан прилетел на 47-м запросе.

Что сделали: перенесли на выделенный сервер в Hetzner Falkenstein с IPv6-прокси, эмуляцию Chrome через Playwright, отключили IPv6-адрес от обратной DNS-записи AWS. Бан ушёл. Скорость — 4 req/s, но стабильно.

Кейс: ротация внутри одного /64

Клиент взял IPv6-прокси-пул и настроил ротацию по 10 000 адресов внутри /64. Через сутки — блокировка всего /64 на Google. Логи показали: Google агрегирует репутацию по /64, и 10 000 «разных» адресов для него — один клиент.

Переделали: запросили пул из 64 разных /48. Ротация по /48, внутри каждого — один адрес. Google перестал банить. Скорость выросла в 6 раз, потому что исчезли капчи.

Что делать в 2025

Голые прокси больше не работают против серьёзного антифрода. Нужна связка: правильный TLS-отпечаток + согласованный HTTP/2-отпечаток + TCP-стек под заявленную ОС + IP с чистой репутацией на уровне /48 + человекоподобное поведение.

Порядок проверки при бане: сначала TLS (JA3/JA4), потом HTTP/2 (Akamai fingerprint, JA4H), потом TCP, потом IP-репутация, потом поведение. В 80% случаев проблема в первых двух пунктах, а не в прокси.

curl_cffi и Playwright закрывают 90% задач. Остальные 10% — это антифроды с кастомными челленджами (Cloudflare Turnstile, DataDome, PerimeterX), где нужен уже полный браузер с реальным рендерингом и человеческими задержками в 200–800 мс между действиями.

Считай бюджет: один запрос через curl_cffi с прокси стоит около 0.3 секунды CPU. Через Playwright — 2–4 секунды. Разница в 10 раз, и это определяет, что ты можешь себе позволить на объёме в миллион страниц.

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