Как антифрод-системы маркетплейсов вычисляют фермы прокси по TLS fingerprint
Содержание
- Что вообще такое TLS fingerprint
- Почему прокси-фермы палятся на ровном месте
- Что смотрят кроме JA3
- Как выглядит детект на практике
- Почему подмена User-Agent не спасает
- Как маскируют отпечатки
- Пример: как посчитать JA3 и сравнить
- Python: проверка своего отпечатка
- Ротация отпечатков и её подводные камни
- Что реально работает
Что вообще такое TLS fingerprint
Когда клиент устанавливает HTTPS-соединение, первым делом летит ClientHello. В нём — версия TLS, список cipher suites, расширения, группы эллиптических кривых, порядок байтов. Набор этих параметров зависит от библиотеки и её версии: OpenSSL 1.1.1, BoringSSL, NSS, Go crypto/tls, Python ssl — все они формируют разные отпечатки.
JA3 — хеш от конкатенации полей ClientHello. JA4 — развитие идеи, учитывает ещё и ALPN, формат SNI, версию. Сервер считает хеш до всякой аутентификации. Прокси-ферма из тысячи нод, где все ноды гонят трафик через один и тот же Go-клиент — это тысяча одинаковых JA4-хешей. Для антифрода это красный флаг размером с дом.
Дальше начинается интересное. Само по себе совпадение отпечатков не приговор — миллионы людей сидят на Chrome 120 и имеют одинаковый JA3. Проблема в контексте: если с одного JA3-хеша идут запросы с 4000 разных IP, в 200 разных ASN, и все логинятся в аккаунты с поведением ботов — картина складывается.
Почему прокси-фермы палятся на ровном месте
Классическая ферма: купили пул residential-прокси, накатили на каждую ноду curl или requests, раздали задачи. Все ноды используют системный OpenSSL одной версии. JA3 у всех идентичный. Даже если IP-адреса чистые и геолокация совпадает — отпечаток выдаёт.
Маркетплейсы (Ozon, Wildberries, Amazon, eBay) держат собственные антифрод-команды. У них есть baseline: распределение JA3/JA4 по легитимным пользователям. Chrome даёт один хеш, Safari другой, мобильные приложения — третий. Если появляется кластер из 5000 сессий с хешем, которого в baseline 0.001% — это аномалия.
Библиотека requests в Python 3.11 тянет OpenSSL 3.0.x. JA3 для неё стабилен годами. Go net/http — свой crypto/tls, свой отпечаток. Node.js — OpenSSL, но с другими настройками по умолчанию. Все эти отпечатки известны антифроду. Они буквально держат таблицу: «вот JA3 curl 7.88, вот JA3 curl 8.4, вот JA3 python-requests 2.31».
Что смотрят кроме JA3
TLS — только верхушка. Антифрод собирает десятки сигналов и склеивает их в единый device fingerprint.
HTTP/2 fingerprint: SETTINGS-фрейм, порядок заголовков, приоритеты потоков, размер window update. У curl и у Chrome они разные. Chrome шлёт SETTINGS с конкретными значениями INITIAL_WINDOW_SIZE, MAX_CONCURRENT_STREAMS, а curl — другие. Плюс порядок псевдозаголовков (:method, :authority, :scheme, :path) — Chrome использует один, Firefox другой, библиотеки — третий.
TCP/IP fingerprint: TTL, размер окна, опции (MSS, SACK, timestamps), начальный sequence number. Если прокси-нода — Linux-сервер, а User-Agent говорит Windows 10 — несоответствие. p0f и аналоги ловят это на раз.
Порядок и набор HTTP-заголовков. Chrome всегда шлёт Accept-Language с q-факторами, sec-ch-ua, sec-fetch-*. Python-requests по умолчанию шлёт только Host, User-Agent, Accept-Encoding, Connection. Если видишь «User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/120» и при этом нет sec-ch-ua — это палево.
Как выглядит детект на практике
Пример: маркетплейс видит 3000 сессий за час. Все с JA4-хешем, соответствующим Go 1.21. Все с HTTP/2-фреймами, характерными для Go net/http. Все с одинаковым порядком заголовков. IP разные, ASN разные, User-Agent рандомизирован. Но три нижних слоя — TLS, HTTP/2, TCP — идентичны.
Скор такой группы зашкаливает. Дальше антифрод применяет графовый анализ: строит связи между аккаунтами, платёжными методами, адресами доставки. Если 200 аккаунтов с одним JA4 шлют заказы на 15 адресов — блокировка пачкой.
Другой сценарий: ферма маскируется под мобильные приложения. User-Agent — «okhttp/4.9.3», а TLS-отпечаток — OpenSSL 3.0 без расширений, которые OkHttp добавляет. Несоответствие ловится моментально. OkHttp шлёт конкретный набор расширений (ALPN с h2, supported_groups с X25519), и подделать его случайным OpenSSL-клиентом не выйдет.
Почему подмена User-Agent не спасает
Новички думают: поставлю User-Agent от Chrome — и всё. Но User-Agent — это одна строка в HTTP-заголовке. Антифрод смотрит на 50+ сигналов. Подмена одной строки при неизменном TLS-отпечатке создаёт ещё более явную аномалию: «Chrome 120 с JA3 от python-requests». Такой профиль не существует у реальных пользователей.
Хуже того — антифроды начали использовать активные проверки. JavaScript-челлендж на странице собирает canvas fingerprint, WebGL-рендеринг, список шрифтов, AudioContext. Если запросы идут без исполнения JS (curl, requests) — сразу видно. Если через headless-браузер — палится по другим признакам: отсутствие плагинов, WebDriver-флаги, нестандартные размеры экрана.
TLS-отпечаток в этой цепочке — самый ранний и самый дешёвый сигнал. Его считают на балансировщике до маршрутизации. Стоимость — микросекунды на запрос. Поэтому его используют как первичный фильтр.
Как маскируют отпечатки
Есть несколько уровней сложности.
Первый — использовать библиотеки с реалистичными отпечатками. curl-impersonate патчит curl, чтобы он повторял JA3 и HTTP/2-фреймы Chrome или Firefox. Аналогично — tls-client на Go, curl_cffi на Python. Они подменяют cipher suites, расширения, порядок заголовков.
Второй — рандомизировать отпечаток на каждой ноде. Менять набор cipher suites, порядок расширений, версию TLS в допустимых пределах. Тогда ферма не выглядит монолитом.
Третий — реальные браузеры. Playwright с patchright, undetected-chromedriver, антидетект-браузеры. Дорого по ресурсам, но отпечаток настоящий.
Проблема в том, что антифроды тоже не стоят на месте. Они строят модели, которые ловят не только точное совпадение, но и статистические аномалии. Если 500 нод шлют JA3 с редкими расширениями, которых нет ни у одного реального браузера — это палево, даже если хеши разные.
Пример: как посчитать JA3 и сравнить
Считаем JA3 для двух клиентов — curl и Chrome — и смотрим разницу.
```bash
curl -s https://ja3er.com/json | jq .
```
Вывод для curl 8.4 на Ubuntu 22.04:
```json
{
"ja3_hash": "3b5074b1b5d032e5620f69f9f700ff0e",
"ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0"
}
```
Тот же запрос из Chrome 120:
```json
{
"ja3_hash": "cd08e31494f9531f560d64c695473da9",
"ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21-41,29-23-24,0"
}
```
Разница — в расширениях (Chrome добавляет 21 и 41 — padding и pre_shared_key) и в группах. JA3-хеш разный. Антифрод видит: с этого IP идёт curl-отпечаток, а User-Agent заявлен как Chrome. Вердикт — бот.
Чтобы привести curl к Chrome-отпечатку:
```bash
curl-impersonate-chrome -s https://ja3er.com/json
```
curl-impersonate даёт JA3, совпадающий с Chrome. Но HTTP/2-фреймы всё равно могут отличаться — их тоже надо патчить, иначе антифрод поймает на втором слое.
Python: проверка своего отпечатка
Скрипт для аудита: какой отпечаток отдаёт ваш клиент.
```python
import ssl
import socket
import hashlib
def get_ja3(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:
cipher = ssock.cipher()
version = ssock.version()
return {
"version": version,
"cipher": cipher[0] if cipher else None,
"tls_version_num": ssock.version()
}
print(get_ja3("ja3er.com"))
```
Этот код покажет только negotiated cipher, не полный ClientHello. Для полного JA3 нужен либо scapy с перехватом, либо серверная сторона — например, nginx с модулем ssl_ja3 или собственный listener.
Более практичный подход — отправить запрос на сервис, который считает JA3 и возвращает результат:
```python
import requests
resp = requests.get("https://ja3er.com/json", timeout=10)
data = resp.json()
print(f"JA3 hash: {data['ja3_hash']}")
print(f"JA3 raw: {data['ja3']}")
```
Запустите это на каждой ноде фермы. Если хеши совпадают — антифрод увидит монолит. Если различаются, но все соответствуют «python-requests» — тоже увидит. Реалистичный отпечаток требует либо браузера, либо tls-client с профилем Chrome.
Ротация отпечатков и её подводные камни
Идея: менять TLS-отпечаток на каждой ноде, чтобы ферма не выглядела однородной. На практике — грабли.
Первое: отпечаток должен соответствовать User-Agent. Если JA3 от Firefox, а UA от Chrome — несоответствие. Нужно менять оба синхронно, а ещё HTTP/2-фреймы, порядок заголовков, набор sec-ch-ua. Это уже не «поменять одну настройку», а полноценный профиль клиента.
Второе: антифроды знают редкие отпечатки. Если вы используете JA3, который встречается у 0.0001% пользователей — вы аномалия, даже если он «валидный». Безопаснее мимикрировать под массовые браузеры: Chrome, Safari, Firefox последних версий.
Третье: поведенческие сигналы. Даже идеальный отпечаток Chrome не спасёт, если запросы идут с интервалом ровно 1000 мс, без движений мыши, с прямыми переходами по URL. Антифрод смотрит на timing, на порядок запросов, на наличие/отсутствие статики.
На lexic.ml при работе с пулами IPv6 приходится учитывать, что один и тот же TLS-отпечаток на разных /64-префиксах выглядит подозрительно. Антифроды группируют по ASN и по префиксу, и однородный JA4 внутри одной подсети — сигнал.
Что реально работает
Полный стек маскировки выглядит так: реальный браузер (или его точная эмуляция) + residential/mobile прокси + рандомизация поведения + соответствие всех слоёв отпечатка. Пропустить хоть один слой — палево.
Практика показывает: дешевле и надёжнее использовать настоящие браузеры через Playwright с патчами, чем строить свою эмуляцию TLS+HTTP/2+TCP. Патченный Chromium даёт корректный отпечаток на всех слоях автоматически. Прокси-ферма из curl-нод с подменой JA3 живёт до первого серьёзного апдейта антифрода — обычно пара недель.
Маркетплейсы обновляют модели регулярно. То, что работало в январе, в марте может баниться пачками. Единственная устойчивая стратегия — не пытаться обмануть отдельный сигнал, а выглядеть как обычный пользователь целиком.