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

Как антифрод-системы маркетплейсов вычисляют фермы прокси по TLS fingerprint

Как антифрод-системы маркетплейсов вычисляют фермы прокси по TLS fingerprint

Что вообще такое 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 живёт до первого серьёзного апдейта антифрода — обычно пара недель.

Маркетплейсы обновляют модели регулярно. То, что работало в январе, в марте может баниться пачками. Единственная устойчивая стратегия — не пытаться обмануть отдельный сигнал, а выглядеть как обычный пользователь целиком.

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