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

- Как антибот-системы ловят прокси по TLS-отпечатку (JA3/JA4) и что меняет ротация IP

- Как антибот-системы ловят прокси по TLS-отпечатку (JA3/JA4) и что меняет ротация IP

Что вообще видит сервер

Когда curl стучится на сайт по HTTPS, первым делом уходит TCP-хендшейк, потом TLS ClientHello. В этом ClientHello — набор шифров, расширений, эллиптических кривых, порядок байтов. Всё это железно зависит от библиотеки и её версии. OpenSSL 1.1.1 и OpenSSL 3.0 отправят разные приветствия. Python requests на своём urllib3 — третье. Chrome — четвёртое.

Антиботу не нужно ломать шифрование. Ему достаточно прочитать ClientHello, посчитать хэш от набора параметров — и сравнить с базой известных отпечатков. Если в заголовках написано «Chrome/120», а отпечаток принадлежит python-requests 2.31 — это красный флаг. Ротация IP тут не спасает: новый адрес, тот же отпечаток.

JA3 — это MD5 от строки вида `TLSVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats`. JA4 — уже SHA256, с нормализацией порядка расширений и разделением на части (JA4a для TLS, JA4h для HTTP/2, JA4t для TCP). JA4 устойчивее к рандомизации порядка расширений — Chrome как раз рандомизирует, но JA4 это переживает.

Почему ротация IP — не панацея

Прокси-пул на 10 000 адресов звучит солидно. Пока не поймёшь, что все 10 000 запросов идут с одинаковым JA3. Антибот строит граф: «этот отпечаток встречается на 10 000 IP, каждый IP делает 50 запросов в час, все запросы с одинаковыми заголовками». Кластерный анализ — и весь пул в чёрном списке за пару суток.

Есть и второй слой. Поведенческий. Ротация IP меняет адрес, но не меняет тайминги: задержку между запросами, jitter, порядок обхода страниц, размеры пакетов. Если 5000 «разных пользователей» открывают страницу ровно через 340 мс после загрузки предыдущей — это бот, и никакой JA3 считать не надо.

Третий слой — TLS-сессия. Если прокси терминирует TLS и переустанавливает соединение к цели, у него свой стек. Часто это OpenSSL из состава squid или 3proxy. Отпечаток получается «прокси-специфичный», его знают все антибот-вендоры. Ротация IP здесь вообще ни при чём — отпечаток один на весь пул.

Как выглядит JA3 вживую

Считается он тривиально. Вот рабочий сниффер на Python, который слушает ClientHello и печатает JA3:

```python

import socket, struct, hashlib

def parse_client_hello(data):

data: raw TLS record starting from handshake

if data[0] != 0x16:

return None

skip record header (5 bytes), handshake header (4 bytes)

p = 5 + 4

version = data[p:p+2].hex()

p += 2 + 32 # version + random

sid_len = data[p]

p += 1 + sid_len

cs_len = struct.unpack(">H", data[p:p+2])[0]

p += 2

ciphers = data[p:p+cs_len]

p += cs_len

comp_len = data[p]

p += 1 + comp_len

if p >= len(data):

return version, ciphers.hex(), "", ""

ext_len = struct.unpack(">H", data[p:p+2])[0]

p += 2

exts, curves, ecpf = [], [], []

end = p + ext_len

while p < end:

etype = struct.unpack(">H", data[p:p+2])[0]

elen = struct.unpack(">H", data[p+2:p+4])[0]

body = data[p+4:p+4+elen]

exts.append(etype)

if etype == 10 and len(body) >= 2:

cl = struct.unpack(">H", body[:2])[0]

curves = [struct.unpack(">H", body[2+i:4+i])[0] for i in range(0, cl, 2)]

if etype == 11 and len(body) >= 1:

ecpf = list(body[1:1+body[0]])

p += 4 + elen

return version, ciphers.hex(), ",".join(map(str, exts)), ",".join(map(str, curves))

def ja3(version, ciphers, exts, curves):

raw = f"{int(version,16)},{ciphers},{exts},{curves},0"

return hashlib.md5(raw.encode()).hexdigest()

s = socket.socket()

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(("0.0.0.0", 8443))

s.listen(1)

conn, _ = s.accept()

data = conn.recv(4096)

v, c, e, cu = parse_client_hello(data)

print("JA3:", ja3(v, c, e, cu))

```

Запускаешь, стучишься `curl -k https://localhost:8443` — получаешь `b32309a26951912be7dba376398abc3b`. Это дефолтный curl на OpenSSL 3.x. Chrome даст совсем другой хэш. Список известных JA3 лежит в открытом репозитории `salesforce/ja3` — там же таблица соответствия «хэш → библиотека».

Что делает ротация и чего не делает

Ротация IP решает ровно одну задачу: размазывает rate-limit по адресам. Если сайт блокирует по «100 запросов с одного IP в минуту», пул из 200 адресов даст тебе 20 000 запросов. Полезно. Но антиботы давно не считают только по IP.

Вот что ротация НЕ меняет: JA3/JA4, порядок HTTP-заголовков (JA4h), HTTP/2 fingerprint (настройки SETTINGS-фрейма: initial window size, max concurrent streams), TCP-опции (TTL, window size — JA4t), поведение при редиректах, cookie-жара. Всё это привязано к клиенту, а не к адресу.

Практический вывод: ротация IP нужна, но она — второй эшелон. Первый — сделать так, чтобы отпечаток клиента выглядел как отпечаток браузера. Иначе 10 000 чистых адресов сгорят за неделю.

Кейс: парсер маркетплейса на 50 000 запросов в сутки

Была задача собирать цены с площадки, которая крутит Cloudflare с включённым Bot Management. Начали с резидентских прокси, 500 адресов, `python-requests`. Первые 2000 запросов прошли. На 2001-м — 403 с `cf-mitigated: challenge`. Все 500 IP в бане за сутки.

Причина: JA3 от `requests` = `3b5074b1b5d032e5620f69f9f700ff0e` (OpenSSL 1.1.1 + urllib3). Cloudflare знает этот хэш наизусть. Плюс заголовки шли в порядке, который `requests` не контролирует: `Host`, `User-Agent`, `Accept-Encoding`, `Accept`, `Connection`. У Chrome порядок другой: `Host`, `Connection`, `sec-ch-ua`, `sec-ch-ua-mobile`, `User-Agent`, `Accept`, `Sec-Fetch-*`, `Accept-Encoding`.

Решение: перешли на `curl_cffi` (Python-биндинг к libcurl с impersonate). Одна строка `impersonate="chrome120"` — и JA3 совпадает с Chrome 120, HTTP/2 fingerprint тоже, порядок заголовков воспроизводится. Плюс оставили ротацию, но уже по 200 адресов на 50 000 запросов — меньше в 2.5 раза. Прокси-пул перестал выгорать: с включённым impersonate блокировки упали с 100% до 3%.

```python

from curl_cffi import requests

r = requests.get(

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

impersonate="chrome120",

proxies={"https": "http://user:pass@proxy.lexic.ml:8080"},

timeout=15,

)

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

```

Обрати внимание: `curl_cffi` подменяет не только TLS, но и HTTP/2 SETTINGS. Без этого Cloudflare ловит по JA4h за один запрос.

Кейс: nginx-реверс с TLS-терминацией

Схема: клиент → наш nginx (TLS termination) → upstream. Nginx переустанавливает соединение к upstream через свой OpenSSL. В логах upstream видит JA3 nginx — `9c8b2f...`, характерный для OpenSSL 3.0 без кастомизации. Антибот upstream-а (если это, скажем, защищённый API) отбивает 100% запросов.

Причина не в IP. Причина в том, что nginx не умеет притворяться браузером — у него фиксированный набор шифров и расширений, заданный в `ssl_ciphers` и `ssl_conf_command`. Даже если выставить `ssl_ciphers` как у Chrome, порядок расширений и набор кривых останутся nginx-овскими.

Лечится либо `proxy_ssl_*` с подбором параметров (частично), либо отказом от TLS-терминации на nginx — пусть клиент говорит с upstream напрямую через CONNECT-прокси. Второй вариант обычно и выбирают: nginx отдаёт 200 на CONNECT, дальше трафик идёт байт-в-байт, отпечаток клиента сохраняется.

```nginx

stream {

server {

listen 443;

proxy_pass backend:443;

proxy_ssl off;

proxy_connect_timeout 5s;

proxy_timeout 60s;

}

}

```

Здесь `proxy_ssl off` — ключевое. Nginx не трогает TLS, просто перекладывает байты. JA3 клиента доходит до цели без изменений.

HTTP/2 fingerprint — то, что забывают

JA3 смотрят на TLS. Но если сайт на HTTP/2 (а сейчас почти все), антибот читает ещё и SETTINGS-фрейм. У Chrome: `HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0, MAX_CONCURRENT_STREAMS=1000, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144`. У python-h2: другие цифры. У Go net/http — третьи.

Cloudflare и Akamai считают JA4h — хэш от этих параметров плюс порядка заголовков. Если TLS ты подделал через curl_cffi, а HTTP/2 оставил дефолтный — тебя поймают на втором слое. `curl_cffi` с impersonate закрывает оба. Голый `httpx` с `http2=True` — только TLS, HTTP/2 останется палевным.

Проверить свой отпечаток можно на `tls.browserleaks.com` или `ja3er.com`. Открой сайт из браузера и из своего скрипта — сравни хэши. Разница в первых символах = разный стек.

Когда ротация всё-таки главное

Есть класс антиботов, которые вообще не смотрят на TLS. Простые rate-limit по IP, гео-фильтры, лимиты на подписку. Тут ротация решает всё, и городить impersonate смысла нет.

Пример: API одного сервиса статистики. Лимит — 60 запросов в минуту с IP, TLS не проверяется, User-Agent не важен. Пул из 10 адресов даёт 600 запросов в минуту. Задача закрыта ротацией, отпечаток не трогаем.

Другой пример: сбор данных с сайтов, где стоит только nginx + `limit_req`. Тут `requests` с ротацией работает годами. Проблемы начинаются, когда владелец сайта ставит Cloudflare «на всякий случай» — и весь пул сгорает за день.

Правило простое: сначала выясни, что именно считает антибот. Если в ответе есть `cf-ray`, `server: cloudflare`, `x-akamai-*` — готовься к отпечаткам. Если просто 429 без заголовков — хватит ротации.

Собираем стек правильно

Порядок действий для парсера, который должен жить долго.

Сначала выбери HTTP-клиент с управляемым TLS. `curl_cffi` — лучший вариант на Python сегодня. `tls-client` (Go-биндинг) — второй по популярности. Голый `requests` и `httpx` — только для сайтов без защиты.

Потом настрой заголовки в правильном порядке. `curl_cffi` с impersonate делает это сам, но если пишешь свой клиент — порядок критичен. Chrome шлёт `sec-ch-ua` до `User-Agent`, Firefox — иначе.

Дальше — прокси. Ротация по сессиям, а не по каждому запросу. Один IP на 5-10 минут, потом смена. Резкая смена адреса между запросами внутри одной «сессии» — сама по себе сигнал.

И последнее — тайминги. Случайная задержка 1.5-4 секунды между запросами, иногда паузы 30-60 секунд. Без этого даже идеальный отпечаток палится по ритму.

Цифры, которые стоит помнить

JA3 от Chrome 120 стабилен и известен всем антиботам — его подделка не даёт преимущества, но её отсутствие палит сразу. JA4 меняется между минорными версиями Chrome — 120 и 121 дают разные хэши, это нормально.

Дефолтный `requests` на Python 3.11 + OpenSSL 3.0 даёт JA3 `3b5074b1b5d032e5620f69f9f700ff0e`. Тот же `requests` на Python 3.8 + OpenSSL 1.1.1 — `3b5074b1b5d032e5620f69f9f700ff0e` тоже, но с другим набором расширений. Хэш совпадает случайно, а вот JA4 — нет.

Cloudflare Bot Management ловит несовпадение UA и JA3 за один запрос. Akamai — за два (второй нужен для подтверждения). DataDome смотрит ещё и на canvas/WebGL, но для API-запросов хватает TLS.

Ротация IP без смены отпечатка продлевает жизнь пула в среднем на 3-7 дней на защищённых сайтах. С правильным отпечатком — на месяцы, пока не поменяется версия Chrome в базе антибота.

Итог

Ротация IP и подмена отпечатка — не альтернативы, а слои. Ротация размазывает нагрузку, отпечаток маскирует клиента. Убрать любой из слоёв — и защита тебя видит. Порядок настройки: сначала клиент с правильным TLS/HTTP2, потом прокси с умной ротацией, потом тайминги. В обратном порядке — деньги на ветер.

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