Как антифрод-системы маркетплейсов вычисляют мультиаккаунты по TLS JA3-отпечатку при использовании прокси
Содержание
- Что вообще такое JA3 и откуда он берётся
- Почему один и тот же клиент даёт стабильный JA3
- Как выглядит связка JA3 + IP + поведение
- Кейс с маркетплейсом электроники
- Кейс с Python-скриптом на SOCKS5
- Что реально меняет JA3
- Почему прокси не решает проблему
- Как выглядит правильная архитектура
- Что делать, если уже спалились
Что вообще такое JA3 и откуда он берётся
TLS-рукопожатие — это обмен ClientHello, ServerHello и кучей расширений до того, как пойдёт хоть один байт HTTP. В ClientHello клиент перечисляет версии TLS, наборы шифров (cipher suites), эллиптические кривые, точки сжатия, расширения и их порядок. Всё это добро у разных клиентов отличается: Chrome 120, Firefox 121, curl 8.5, Python requests 2.31, Go net/http — каждый собирает свой уникальный список.
JA3 берёт пять полей из ClientHello, склеивает их через запятую и через дефис, потом считает MD5. Получается 32-символьный хеш. Пример из реального дампа:
```
769,47-53-5-10-49161-49162-49171-49172-50-56-65-4-5-49195-49199-49196-49200-52393-52392-49188-49192-49187-49191-49198-49202-49197-49201-159-158-107-103-57-51-157-156-61-60-53-47,0-11-10-35-22-23-13-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2
```
MD5 от этой строки — и есть отпечаток. Антифрод маркетплейса считает его на балансировщике или на самом edge-сервере ещё до того, как запрос дойдёт до приложения. Стоит это копейки по CPU, а даёт кучу информации.
Прокси тут ничем не помогает. SOCKS5, HTTP CONNECT, residential — всё это меняет IP, но не трогает содержимое ClientHello. Клиент шлёт свой TLS-hello напрямую через туннель, и сервер видит ровно тот же JA3, что и без прокси.
Почему один и тот же клиент даёт стабильный JA3
Cipher suites и расширения в ClientHello у конкретной сборки браузера жёстко зашиты. Chrome не рандомизирует их от запуска к запуску (рандомизация ClientHello — отдельная история, и она ломает кеширование, поэтому её включают не все). Значит, тысяча аккаунтов, заведённых через одну и ту же сборку Chrome 120 на одной машине, даст один и тот же JA3. Антифрод это видит.
Дальше начинается статистика. Если с одного JA3 за сутки приходит 4000 регистраций с 3800 разных IP — это не 3800 людей. Это один автоматизированный зоопарк. Порог срабатывания у всех разный, но логика одинаковая: JA3 редко встречается у живых людей в таких объёмах.
Отдельно берут JA3S — отпечаток ServerHello. Он помогает определить, к какому серверу шёл клиент, и ловит цепочки прокси, где промежуточный узел имеет свой характерный TLS-стек.
Как выглядит связка JA3 + IP + поведение
Сам по себе JA3 — слабый сигнал. Сила появляется в корреляции. Классическая схема на стороне маркетплейса:
```python
from collections import defaultdict
def risk_score(event):
score = 0
ja3 = event["ja3"]
ip = event["ip"]
ua = event["user_agent"]
if ja3_cluster_size(ja3, window="1h") > 50:
score += 30
if ja3 not in COMMON_BROWSER_JA3:
score += 25
if ua_mismatch_ja3(ua, ja3):
score += 40
if ip_reputation(ip) < 0.3:
score += 20
if event["asn"] in DATACENTER_ASN:
score += 15
return score
```
`ua_mismatch_ja3` — ключевая проверка. Если User-Agent говорит «Chrome 120 на Windows», а JA3 соответствует curl или Python — это красный флаг. Такой мисматч почти всегда означает автоматизацию. Нормальный Chrome на Windows шлёт конкретный набор из 16 cipher suites в определённом порядке, и подделать его без патча библиотеки нельзя.
Прокси тут опять не спасает. IP residential, ASN домашнего провайдера, UA честный — а JA3 от requests. Всё, аккаунт помечен.
Кейс с маркетплейсом электроники
Маркетплейс с оборотом около 2 млн заказов в месяц. Антифрод на nginx 1.24 с модулем, который логирует JA3 в отдельный поток. Порог: JA3, встречающийся больше 200 раз за час с более чем 50 уникальных IP, попадает в мониторинг.
Прилетела волна: 1200 регистраций за 40 минут. IP — 900 разных, все residential, гео разбросано по 12 регионам. ASN — домашние провайдеры. По IP чисто.
JA3 у всех 1200 — один. И это не Chrome. Это отпечаток, который даёт `curl_cffi` с импersonation Chrome 116, но с одной кривой конфигурацией: порядок расширений не совпадал с настоящим Chrome. Антифрод сравнил с базой из 40 тысяч реальных Chrome-сессий — совпадений ноль.
Дальше руками: посмотрели ASN-распределение, время между регистрациями (медиана 1.8 секунды), одинаковые паттерны заполнения профиля. Забанили всю волну. Причина провала — не прокси, а то, что имитация TLS была неполной.
Кейс с Python-скриптом на SOCKS5
Продавец запускал парсер цен через пул из 500 residential SOCKS5-прокси. Скрипт на Python 3.11, библиотека `requests` 2.31, TLS через OpenSSL 3.0. JA3 — стабильный, привязан к версии OpenSSL и настройкам `requests`.
Маркетплейс видел: 500 IP, все residential, но 500 сессий с одним JA3 за короткое время. Плюс `requests` по умолчанию не шлёт HTTP/2 — только HTTP/1.1. Настоящий Chrome шлёт HTTP/2. Это второй сигнал поверх JA3.
Через 3 дня пул прокси сгорел целиком. Не потому что IP плохие, а потому что TLS-отпечаток один на все 500. Антифрод просто связал все аккаунты в один кластер и забанил по кластеру.
Решение тут — `curl_cffi` или `tls-client`, которые умеют импersonate реальные браузеры. Но и они не панацея: если версия Chrome в импersonate устарела, JA3 не совпадёт с актуальной базой антифрода.
Что реально меняет JA3
Список инструментов, которые действительно подменяют TLS-отпечаток:
| Инструмент | Что делает | Ограничения |
|---|---|---|
| curl_cffi | Импersonate Chrome/Firefox/Safari | Отстаёт от свежих версий браузеров |
| tls-client (Go) | То же, с HTTP/2 | Меньше профилей |
| utls (Go) | Ручная настройка ClientHello | Нужно знать актуальный профиль |
| NSS + патч | Полный контроль | Сложно поддерживать |
| Кастомный OpenSSL | Полный контроль | Ещё сложнее |
Обычный `requests`, `aiohttp`, `httpx` — не меняют. SOCKS5-прокси — не меняют. HTTP-прокси с CONNECT — не меняют. Rotating residential — не меняют. Всё, что работает на уровне IP, к TLS-отпечатку отношения не имеет.
Вот как выглядит проверка своего JA3 через Python с `curl_cffi`:
```python
from curl_cffi import requests
r = requests.get(
"https://tls.browserleaks.com/json",
impersonate="chrome120",
proxies={"https": "socks5://user:pass@host:1080"},
)
print(r.json()["ja3_hash"])
```
Если хеш совпадает с эталонным для Chrome 120 — хорошо. Если нет — антифрод увидит разницу.
Почему прокси не решает проблему
Прокси меняет сетевой уровень. JA3 живёт на транспортном, внутри TLS. Это разные слои, и подмена одного не влияет на другой. Маркетплейс видит IP `203.0.113.45` (residential, Comcast, США) и JA3 `cd08e31494f9531f560d64c695473da9` (Python requests). Комбинация невозможна у живого пользователя: браузер на домашнем IP не может иметь JA3 от Python.
Антифрод-системы давно перешли от проверки «IP в датацентре / не в датацентре» к мультисигнальным моделям. JA3, JA4, HTTP/2 fingerprint (SETTINGS frame, приоритеты, window size), TCP fingerprint (TTL, window size, MSS), порядок заголовков HTTP — всё это складывается в отпечаток клиента. Прокси закрывает один сигнал из десяти.
Для маркетплейсов это особенно критично: там деньги, отзывы, рейтинги, конкуренция между продавцами. Антифрод настроен жёстко и обновляется постоянно. Схема «residential прокси + обычный requests» перестала работать году в 2021.
Как выглядит правильная архитектура
Правильная схема — это не прокси, а полная имитация браузера. TLS-стек должен соответствовать заявленному User-Agent. HTTP/2 fingerprint — тоже. Порядок заголовков — тоже. Плюс куки, localStorage, canvas, WebGL, если речь о headless-браузере.
Связка, которая работает: реальный браузер (Playwright, Puppeteer) + residential прокси + отключённые утечки WebRTC + правильный fingerprint. Или `curl_cffi` с актуальным импersonate + HTTP/2 + прокси на уровне соединения.
Прокси в этой схеме — только одна из частей. Если менять IP, но оставлять JA3 от Python — толку ноль. Если менять JA3, но оставлять HTTP/1.1 и порядок заголовков от `requests` — тоже ноль. Антифрод смотрит на всё сразу.
Для задач, где нужен именно сетевой уровень — распределённые запросы, гео-тестирование, сбор публичных данных без авторизации — IPv6-прокси с большим пулом адресов дают то, что нужно: разнообразие IP без затрат на residential. Сервис вроде lexic.ml держит пул с 2015 года, и там адреса не пересекаются между пользователями, что важно для чистоты репутации. Но JA3 это не меняет — его надо настраивать на стороне клиента.
Что делать, если уже спалились
Если аккаунты уже связаны по JA3 — менять прокси бессмысленно. Нужно менять TLS-стек. И не только его: HTTP/2 fingerprint, порядок заголовков, тайминги запросов. Антифрод хранит историю, и кластер, собранный по старому JA3, останется.
Практический минимум:
- Перейти на `curl_cffi` или `tls-client` с актуальным профилем браузера
- Включить HTTP/2
- Синхронизировать User-Agent с реальным профилем импersonate
- Проверить JA3 через browserleaks или аналогичный сервис
- Убедиться, что порядок заголовков совпадает с браузером
- Разнести тайминги запросов, не бить в одну секунду
Прокси при этом остаются нужны — для распределения по IP и ASN. Но они не решают проблему отпечатка. Это два разных уровня, и путать их — классические грабли.