Как антифрод-системы маркетплейсов вычисляют IPv6 прокси по TLS-отпечатку JA4
Содержание
- Что вообще такое JA4 и почему он появился
- Как JA4 ловит подмену клиента
- Почему IPv6-прокси особенно уязвимы
- Разбор реального ClientHello
- Подмена JA4: что работает и что нет
- Таблица: сравнение JA4 разных стеков
- Как маркетплейсы строят профиль по JA4
- Что делать с IPv6-прокси, чтобы не палиться
- Кейс: парсинг Wildberries через IPv6-прокси
- Кейс: Amazon и GREASE
- Кейс: Ozon и HTTP/3
- Итоговая стратегия
Что вообще такое JA4 и почему он появился
Старый добрый JA3 от Salesforce приказал долго жить. Проблема в том, что JA3 собирает ClientHello и хеширует его целиком — включая расширения в том порядке, в котором их прислал клиент. Chrome в версии 110 отправляет расширения в одном порядке, в версии 120 — уже в другом. Хеш меняется от релиза к релизу, и антифрод-системы получают кучу ложных срабатываний: живой пользователь обновил браузер, а его отпечаток стал «новым».
FoxIO в 2023 году выпустили JA4 — переработку идеи. Вместо хеша всего ClientHello берётся структурированный отпечаток: тип транспорта, версия TLS, наличие SNI, количество расширений, ALPN, набор шифров и порядок сигнатур. Всё это упаковывается в строку вида `t13d1516h2_8daaf6152771_b186095e22b6`.
Первая часть — метаданные. Вторая — SHA256 от списка шифров и расширений, обрезанный до 12 символов. Третья — хеш от сигнатур. Ключевое отличие: JA4 устойчив к перестановке расширений, но чувствителен к их составу. Именно это делает его опасным для прокси-инфраструктуры.
Как JA4 ловит подмену клиента
Классическая схема обхода: пользователь сидит на прокси, который терминирует TLS и открывает своё соединение к маркетплейсу. Прокси-сервер обычно на Python с библиотекой `requests` или на Go с `net/http`. Их TLS-стек — не Chrome. И вот тут начинается веселье.
JA4 для реального Chrome 124 выглядит примерно так: `t13d1516h2_8daaf6152771_02713d6af862`. Для Python `requests` с `urllib3` — `t13d1715h2_5b57614c22b0_3d5424432f57`. Разница видна сразу: количество шифров (15 против 17), другой набор сигнатур. Антифрод маркетплейса сравнивает JA4 входящего соединения с заявленным User-Agent. Если UA говорит «Chrome», а JA4 — от Python, аккаунт улетает в теневой бан.
IPv6 здесь ни при чём — отпечаток одинаково работает и на v4, и на v6. Проблема в том, что IPv6 прокси часто поднимают на VPS с минимальной конфигурацией, где TLS-стек дефолтный. И это палит всю схему.
Почему IPv6-прокси особенно уязвимы
IPv6-адресация даёт огромные пулы — /64 содержит 18 квинтиллионов адресов. Провайдеры вроде lexic.ml выдают /64 или /48 на клиента, и это позволяет ротировать адреса без пересоздания соединений. Но есть нюанс: многие антифрод-системы давно научились смотреть не на сам адрес, а на его поведение.
Маркетплейсы типа Amazon и Wildberries хранят историю JA4-отпечатков по каждому IPv6-префиксу. Если с /64 за неделю пришло 500 разных аккаунтов, а JA4 у всех одинаковый — это сигнал. Реальные пользователи с одного /64 (например, домашняя сеть провайдера) имеют разные устройства, разные браузеры, разные JA4. Ботнет с одного прокси — один JA4 на всех.
Вторая проблема: IPv6-прокси часто работают через SOCKS5 или HTTP CONNECT. При CONNECT TLS-туннель идёт end-to-end, и JA4 виден только целевой стороне. Но при MITM-прокси (когда прокси расшифровывает трафик для подмены заголовков) — JA4 формируется уже на стороне прокси. И если прокси на Python, все клиенты получают один и тот же отпечаток.
Разбор реального ClientHello
Смотрим, что уходит в сеть. Запускаем tcpdump на интерфейсе и вытаскиваем ClientHello:
```bash
tcpdump -i eth0 -s 0 -w capture.pcap 'tcp port 443 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)'
tshark -r capture.pcap -Y "tls.handshake.type == 1" -T fields -e tls.handshake.extensions_alpn_str -e tls.handshake.ciphersuite
```
Для Python `requests` с `urllib3` 2.x набор шифров выглядит так (в hex): `1301 1302 1303 c02b c02f c02c c030 cca9 cca8 c013 c014 009c 009d 002f 0035`. Пятнадцать шифров. Chrome 124 отправляет `1301 1302 1303 c02b c02f c02c c030 cca9 cca8 c013 c014 009c 009d 002f 0035` — почти то же самое, но добавляет `GREASE`-значения и другой порядок. Хеш расходится.
Вот как выглядит проверка JA4 на стороне сервера через nginx с модулем `nginx-ja4`:
```nginx
server {
listen 443 ssl;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
set \$ja4 \$http_ssl_ja4;
access_log /var/log/nginx/access.log ja4_format;
if (\$ja4 ~ "^t13d1516h2_8daaf6152771") {
set \$client_type "chrome";
}
if (\$ja4 ~ "^t13d1715h2_5b57614c22b0") {
set \$client_type "python";
}
}
```
На практике антифрод маркетплейса делает то же самое, только с базой из миллионов отпечатков. И если ваш прокси выдаёт `python` при UA Chrome — это красный флаг.
Подмена JA4: что работает и что нет
Первый подход — использовать `curl_cffi` вместо `requests`. Эта библиотека линкуется с BoringSSL и умеет имитировать отпечатки Chrome, Firefox, Safari. Проверяем:
```python
from curl_cffi import requests
response = requests.get(
"https://marketplace.example/api/items",
impersonate="chrome124",
proxies={"https": "socks5h://user:pass@[2001:db8::1]:1080"}
)
print(response.status_code)
```
`impersonate="chrome124"` даёт JA4 `t13d1516h2_8daaf6152771_02713d6af862` — совпадает с реальным Chrome. Но есть грабли: `curl_cffi` не поддерживает HTTP/3, а Chrome 124 уже умеет. Если маркетплейс проверяет ALPN и видит только `h2` вместо `h2,h3` — снова палево.
Второй подход — прокси с MITM и подменой ClientHello на лету. Например, `mitmproxy` с аддоном, который переписывает TLS-расширения. Работает, но добавляет 15-30 мс задержки на каждое соединение и требует доверенного сертификата на клиенте. Для массового скрейпинга это дорого.
Третий подход — использовать реальный браузер через Playwright или Selenium с IPv6-прокси. JA4 будет настоящий, но ресурсов жрёт в 20-50 раз больше. Один headless Chrome — это 200-400 МБ RAM. На VPS с 4 ГБ больше десяти инстансов не поднимешь.
Таблица: сравнение JA4 разных стеков
| Стек | JA4 (метаданные) | Шифров | ALPN | Совместимость с Chrome UA |
|------|------------------|--------|------|---------------------------|
| Python requests 2.31 | t13d1715h2 | 17 | h2 | Нет |
| curl_cffi 0.6 (chrome124) | t13d1516h2 | 15 | h2 | Да |
| Go net/http 1.22 | t13d1614h2 | 14 | h2 | Нет |
| Node.js 20 (tls) | t13d1715h2 | 17 | h2 | Частично |
| Chrome 124 (реальный) | t13d1516h2 | 15 | h2,h3 | — |
| Firefox 125 | t13d1616h2 | 16 | h2 | Нет |
Видно, что Go и Node.js палятся не хуже Python. Единственный стек, который даёт точное совпадение — `curl_cffi` с правильной версией impersonate. Но и он не покрывает HTTP/3.
Как маркетплейсы строят профиль по JA4
Антифрод не смотрит на один отпечаток. Он строит временной ряд. Первое соединение с новым JA4 — нейтрально. Второе через 5 минут с того же IPv6 — подозрительно. Десятое за час с одного /64 — бан.
Amazon, например, хранит JA4 вместе с TLS-сессией и HTTP/2 fingerprint (SETTINGS frame, порядок заголовков, приоритеты). Комбинация JA4 + HTTP/2 fingerprint + User-Agent + Accept-Language даёт точность выше 99% при определении автоматизации. И IPv6-ротация тут не спасает: отпечаток привязан к клиенту, а не к адресу.
Ещё один маркер — `GREASE` (Generate Random Extensions And Sustain Extensibility). Chrome и Firefox рандомизируют часть расширений при каждом соединении. Если JA4 не меняется от запроса к запросу — это бот. Python `requests` даёт стабильный JA4. Chrome — плавающий (меняются GREASE-значения, но метаданные остаются).
Что делать с IPv6-прокси, чтобы не палиться
Первое: использовать прокси с end-to-end TLS. SOCKS5 без MITM. Тогда JA4 формируется на клиенте, и вы контролируете его через `curl_cffi` или реальный браузер.
Второе: ротировать не только IPv6-адрес, но и JA4. Для этого можно держать пул из нескольких `curl_cffi` с разными `impersonate` — chrome120, chrome124, firefox125. Каждый даёт свой отпечаток.
Третье: проверять свой JA4 перед работой. Есть публичные сервисы вроде `tls.peet.ws` — они показывают полный отпечаток:
```bash
curl -x socks5h://user:pass@[2001:db8::1]:1080 https://tls.peet.ws/api/all | jq '.tls.ja4'
```
Если вывод не совпадает с ожидаемым браузером — не начинайте работу. Лучше потратить 10 минут на настройку, чем потерять аккаунты.
Четвёртое: следить за HTTP/2 fingerprint. JA4 — только часть картины. Порядок заголовков, приоритеты потоков, размер `SETTINGS` frame — всё это тоже проверяется. `curl_cffi` умеет и это, но требует явной настройки.
Кейс: парсинг Wildberries через IPv6-прокси
Задача: собирать данные о товарах с Wildberries через пул IPv6-прокси. Первая версия — Python `requests` + SOCKS5. Результат: через 200 запросов аккаунты улетели в капчу. Причина: JA4 `t13d1715h2_5b57614c22b0` при UA Chrome 124. Wildberries сравнивает отпечаток с UA и блокирует.
Решение: переписали на `curl_cffi` с `impersonate="chrome124"`. JA4 стал `t13d1516h2_8daaf6152771_02713d6af862`. Плюс добавили ротацию IPv6 внутри /64 каждые 50 запросов. Капча пропала. Скорость: 50 запросов в минуту с одного /64 без блокировок. Задержка выросла на 8 мс из-за BoringSSL вместо OpenSSL.
Вторая проблема: Wildberries проверяет HTTP/2 fingerprint. `curl_cffi` по умолчанию отправляет заголовки в порядке, отличном от Chrome. Пришлось использовать `impersonate` с явным указанием версии и добавить заголовки `sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform` в правильном порядке.
Кейс: Amazon и GREASE
Amazon пошёл дальше. Они проверяют не только JA4, но и наличие GREASE-значений в ClientHello. Python `requests` не отправляет GREASE вообще. Chrome отправляет 2-3 GREASE-расширения с рандомными значениями. Если JA4 говорит «Chrome», а GREASE нет — блок.
`curl_cffi` с `impersonate="chrome124"` отправляет GREASE корректно. Но есть нюанс: GREASE-значения должны меняться между соединениями. Если они статичны — это тоже маркер. Проверяется просто: два запроса подряд, сравнение ClientHello. У `curl_cffi` GREASE рандомизируется, у самописного TLS-стека — часто нет.
Решение для Amazon: использовать `curl_cffi` 0.6+ с `impersonate="chrome124"` и ротацией IPv6 каждые 20 запросов. Плюс задержка 2-5 секунд между запросами, имитация человеческого поведения. Результат: 95% успешных запросов без капчи.
Кейс: Ozon и HTTP/3
Ozon начал требовать HTTP/3 для части эндпоинтов. JA4 для HTTP/3 отличается от HTTP/2: другой ALPN (`h3` вместо `h2`), другой набор расширений. `curl_cffi` HTTP/3 не умеет. Пришлось поднимать реальный Chrome через Playwright с IPv6-прокси.
Конфигурация: Playwright + Chromium 124 + SOCKS5 IPv6. JA4 полностью совпадает с реальным браузером, HTTP/3 работает. Минус: 350 МБ RAM на инстанс, 1.5 секунды на запуск. Для 20 параллельных сессий нужно 8 ГБ RAM. Дорого, но работает.
Альтернатива: использовать `aioquic` с кастомным ClientHello. Сложно, но даёт контроль над каждым байтом. Мы пошли по пути Playwright — быстрее в разработке, хоть и дороже в эксплуатации.
Итоговая стратегия
JA4 — не приговор для IPv6-прокси. Но игнорировать его нельзя. Базовая гигиена: `curl_cffi` вместо `requests`, ротация IPv6 внутри /64, проверка отпечатка перед работой. Для сложных случаев — реальный браузер через Playwright.
IPv6-прокси дают огромный пул адресов, но не решают проблему отпечатка. Антифрод смотрит на TLS-стек, HTTP/2 fingerprint, GREASE, поведение. Комбинация этих факторов даёт точность выше 99%. Единственный способ не палиться — выглядеть как реальный браузер на всех уровнях.