Почему прокси «умирает» после первого запроса к антибот-системе: разбор поведенческого fingerprint на уровне TLS/JA3
Содержание
- Что вообще такое JA3 и откуда он берётся
- Почему прокси тут вообще ни при чём
- JA4 и куда движется индустрия
- HTTP/2 fingerprint — второй слой той же проблемы
- Реальный кейс: парсер маркетплейса
- Почему Selenium и Playwright тоже палятся
- Таблица: типичные JA3 популярных клиентов
- Как проверить свой отпечаток
- Подделка JA3: что работает, а что нет
- Кейс: API-клиент на Go
- Кейс: nginx как реверс-прокси перед парсером
- Что ещё смотрят, кроме JA3
- Практический чеклист перед запуском парсера
- Итог
Прокси купили. Список чистый. Первый запрос проходит. Второй возвращает 403 или капчу. Знакомо? Проблема почти никогда не в IP. Она в том, как ваш клиент пожимает руку серверу.
Антибот-системы давно перестали смотреть только на IP-адрес. Cloudflare, Akamai, DataDome, PerimeterX собирают десятки сигналов ещё до того, как вы отправите первый байт HTTP. TLS-хендшейк — самый жирный из них. И самый недооценённый.
Что вообще такое JA3 и откуда он берётся
JA3 — это хеш от параметров ClientHello. Конкретно: версия TLS, список cipher suites, список расширений, список эллиптических кривых, форматы точек кривых. Всё это склеивается в строку через запятую, потом считается MD5. Получается 32-символьный отпечаток.
Идея простая: разные TLS-библиотеки формируют ClientHello по-разному. Chrome 120 шлёт один набор расширений в одном порядке. Python requests — совсем другой. curl с OpenSSL — третий. Go net/http — четвёртый. По этому набору можно с высокой точностью сказать, чем именно вы отправляете запрос.
Вот как выглядит сырая JA3-строка для дефолтного Python requests (urllib3 + OpenSSL 3.x):
```
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:
```
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-30-25-256-257,0
```
Разница в двух местах: у Chrome есть расширения 21 (padding) и 41 (pre_shared_key), и другой набор групп для key share. Мелочь? Для антибота — пропасть.
Почему прокси тут вообще ни при чём
HTTP-прокси работает на уровне приложения. Он видит уже расшифрованный запрос, перенаправляет его. TLS-сессия устанавливается между вашим клиентом и целевым сервером напрямую — прокси в этом хендшейке не участвует вообще (если это не MITM-прокси с подменой сертификата).
Значит, JA3, который видит Cloudflare, — это JA3 вашего клиента. Python-скрипта. curl из cron. Selenium с дефолтным chromedriver. Прокси тут не прибавляет и не убавляет ничего. Меняете IP — fingerprint остаётся тем же.
Отсюда классический сценарий: купили пул из 500 адресов, запустили requests.get() в цикле с ротацией. Первый запрос — 200. Второй — 403. Сменили IP — снова 200 на один раз. Потому что антибот видит: с разных IP приходит один и тот же нечеловеческий JA3. И помечает весь диапазон.
JA4 и куда движется индустрия
JA3 взломали. Точнее, его стало слишком легко подделать и слишком легко заспуфить. В 2023 появился JA4 — от FoxIO. Он учитывает больше параметров: ALPN, SNI (да/нет), версии, порядок расширений с учётом GREASE, сортировку cipher suites.
JA4 выглядит как `t13d1516h2_8daaf6152771_b186095e22b6`. Три части: тип+версия+ALPN+количество, потом хеш cipher suites, потом хеш расширений. Устойчивее к рандомизации, чем JA3.
Крупные антиботы уже частично мигрировали. Cloudflare использует свой набор сигналов, включая JA4 и собственные эвристики на уровне QUIC. Akamai смотрит на JA3 + JA4 + HTTP/2 fingerprint (SETTINGS-фрейм, порядок заголовков, приоритеты потоков).
HTTP/2 fingerprint — второй слой той же проблемы
Если вы ходите по HTTP/2 (а современные сайты почти все), добавляется ещё один отпечаток. Он собирается из:
- SETTINGS-фрейма (значения MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, MAX_FRAME_SIZE)
- порядка и размера WINDOW_UPDATE
- приоритетов потоков
- порядка псевдозаголовков (:method, :path, :authority, :scheme)
Go-клиент шлёт один SETTINGS, nghttp2 — другой, Chrome — третий. Антибот видит: JA3 говорит «Chrome», а HTTP/2-фреймы — «Python httpx». Несоответствие = бан.
Реальный кейс: парсер маркетплейса
Задача: собрать цены с крупного маркетплейса, 50k запросов в сутки. Стек: Python 3.11, requests 2.31, ротация через пул из 200 резидентных прокси.
Первый прогон: 200 OK на 40% запросов, потом 403 с заголовком `cf-mitigated: challenge`. Прокси чистые, проверены через отдельный чекер. Проблема не в IP.
Смотрим, что видит сервер. Запускаем локально `openssl s_client -connect target:443 -servername target` и сравниваем с реальным Chrome. JA3 requests: `51c64c77e60f3980eea90869b68c58a8`. JA3 Chrome 120: `cd08e31494f9531f560d64c695473da9`. Разные. Cloudflare это видит и включает JS-челлендж на каждый второй запрос.
Решение: подмена TLS-стека. Взяли `curl_cffi` (Python-биндинг к curl-impersonate). Он умеет имитировать ClientHello Chrome, Firefox, Safari на уровне байтов.
```python
from curl_cffi import requests
resp = requests.get(
"https://target.example/api/prices",
impersonate="chrome120",
proxies={"https": "http://user:pass@proxy.lexic.ml:8080"},
timeout=15,
)
print(resp.status_code, resp.json().get("items", [])[:2])
```
После замены requests на curl_cffi с `impersonate="chrome120"` доля успешных ответов выросла с 40% до 94%. Тот же пул прокси, те же IP. Изменился только JA3 и HTTP/2 fingerprint.
Почему Selenium и Playwright тоже палятся
Казалось бы, живой браузер — живой fingerprint. Не совсем.
Дефолтный chromedriver поднимает Chrome с флагами `--enable-automation`, `--disable-blink-features=AutomationControlled` не выставлен. Chrome шлёт ClientHello как Chrome, но:
- `navigator.webdriver = true`
- порядок заголовков в HTTP/2 отличается от «чистого» Chrome (CDP-инъекции меняют тайминги)
- отсутствуют некоторые расширения, которые есть в обычном профиле
- TLS-сессия переиспользуется агрессивно, session ticket выглядит необычно
Антиботы (особенно DataDome и PerimeterX) сравнивают JA3/JA4 с набором JS-сигналов. Если TLS говорит «Chrome 120», а `navigator.userAgentData.brands` пустой — несоответствие.
Лечится через `undetected-chromedriver` или `patchright`, но это гонка вооружений. Каждый релиз Chrome ломает предыдущие патчи.
Таблица: типичные JA3 популярных клиентов
| Клиент | JA3 (пример) | Что видит антибот |
|---|---|---|
| Python requests 2.31 | `51c64c77e60f3980eea90869b68c58a8` | «скрипт» |
| curl 8.x (OpenSSL 3) | `3b5074b1b5d032e5620f69f9f700ff0e` | «скрипт/CLI» |
| Go net/http 1.21 | `c9b3c2f1e0a5b8d7c6e4f3a2b1c0d9e8` | «бэкенд» |
| Chrome 120 | `cd08e31494f9531f560d64c695473da9` | «браузер» |
| Firefox 121 | `6c02b3e1c7a3e5e8f4d2b1a9c8e7f6d5` | «браузер» |
| curl_cffi chrome120 | `cd08e31494f9531f560d64c695473da9` | «браузер» |
Цифры JA3 для Go и Firefox приведены как порядок величины — реальные значения зависят от версии и сборки. Ключевое: первые три категорически отличаются от браузерных.
Как проверить свой отпечаток
Быстрый способ — публичные эндпоинты. Например, `tls.peet.ws` возвращает полный разбор ClientHello и HTTP/2.
```bash
curl -s https://tls.peet.ws/api/all | jq '{ja3: .tls.ja3_hash, ja4: .tls.ja4, h2: .http2.akamai_fingerprint}'
```
Ответ покажет три вещи: JA3-хеш, JA4-строку, Akamai HTTP/2 fingerprint. Сравните с тем, что отдаёт реальный Chrome с того же IP. Если расходится — вот ваша причина 403.
Второй способ — `browserleaks.com/tls`. Показывает то же самое плюс расшифровку каждого поля ClientHello. Полезно, когда надо понять, какое именно расширение выдаёт.
Подделка JA3: что работает, а что нет
Спуфинг JA3 на уровне Python — через monkey-patching ssl-модуля — почти бесполезен. Меняете список cipher suites, но порядок расширений остаётся питоновским. Антибот сверяет и то, и другое.
Работающие подходы:
1. **curl-impersonate / curl_cffi** — патченный curl, который на уровне BoringSSL собирает ClientHello байт-в-байт как Chrome. Обновляется вслед за релизами браузеров.
2. **uTLS (Go)** — библиотека от refraction-networking, позволяет собрать ClientHello вручную. Гибко, но требует времени на настройку каждой версии.
3. **Реальный браузер через CDP** — Playwright/Puppeteer с patchright. Дорого по ресурсам, но fingerprint честный.
Чего делать не стоит: ставить рандомизаторы TLS (есть такие npm-пакеты). Случайный JA3 — это JA3, которого не существует ни у одного браузера. Антибот такое ловит мгновенно, потому что «неизвестный клиент» — сам по себе красный флаг.
Кейс: API-клиент на Go
Сервис на Go 1.21 ходил к публичному API через `http.Client` с кастомным `Transport`. Пул прокси на 100 адресов, ротация по Round-Robin. Через сутки — стабильные 429 с `Retry-After: 3600`.
Причина: Go net/http шлёт ClientHello с cipher suites в порядке, который не совпадает ни с одним браузером. Плюс HTTP/2 SETTINGS с `MAX_CONCURRENT_STREAMS=250` — характерное значение Go, у Chrome оно 1000. Антибот (оказался DataDome) склеил эти два сигнала и забанил весь ASN прокси-пула.
Решение: заменили транспорт на `utls` с профилем `HelloChrome_120`, плюс подкрутили HTTP/2-настройки через `http2.Transport` с кастомным `MaxConcurrentStreams: 1000`.
```go
package main
import (
"context"
"fmt"
"net/http"
"time"
tls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
)
func main() {
dialer := &net.Dialer{Timeout: 10 * time.Second}
tr := &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
raw, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
u := tls.UClient(raw, &tls.Config{ServerName: "target.example"}, tls.HelloChrome_120)
if err := u.HandshakeContext(ctx); err != nil {
return nil, err
}
return u, nil
},
MaxReadFrameSize: 1 << 20,
}
client := &http.Client{Transport: tr, Timeout: 20 * time.Second}
resp, err := client.Get("https://target.example/v1/items")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
```
После правки 429 пропали. Пропускная способность выросла в 3.5 раза — с 12 до 42 запросов в секунду на тот же пул прокси.
Кейс: nginx как реверс-прокси перед парсером
Схема: парсер на Python → локальный nginx → внешний прокси → целевой сайт. Nginx терминирует TLS от парсера и открывает новое TLS-соединение к цели через `proxy_pass`. Казалось бы, nginx сгладит fingerprint.
Не сглаживает. Nginx с дефолтным `proxy_ssl_protocols TLSv1.2 TLSv1.3` и OpenSSL 3.0 шлёт JA3 `3b5074b1b5d032e5620f69f9f700ff0e` — типичный OpenSSL. Антибот видит «OpenSSL-клиент без браузерных расширений» и режет.
Плюс nginx по умолчанию не передаёт SNI в апстрим, если не указан `proxy_ssl_server_name on`. Без SNI многие антиботы возвращают 403 на уровне TLS — ещё до HTTP.
Правки, которые помогли:
```nginx
location /api/ {
proxy_pass https://target.example;
proxy_ssl_server_name on;
proxy_ssl_name target.example;
proxy_ssl_protocols TLSv1.3;
proxy_ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
proxy_http_version 2;
proxy_set_header Host target.example;
proxy_set_header User-Agent \$http_user_agent;
proxy_set_header Accept-Language en-US,en;q=0.9;
proxy_buffering off;
}
```
Но даже с этими настройками JA3 остаётся OpenSSL-ным. Если антибот строгий — придётся ставить перед nginx curl_cffi или nginx-модуль с BoringSSL. Обычный nginx-конфиг JA3 не подделает, это надо понимать заранее.
Что ещё смотрят, кроме JA3
TLS — не единственный поведенческий сигнал. Антиботы собирают:
- **Тайминги.** Человек не делает 200 запросов подряд с интервалом 50 мс. Задержка между TLS-хендшейком и первым HTTP-запросом у браузера — 80-300 мс, у скрипта — часто меньше 10 мс.
- **Порядок заголовков.** Chrome шлёт `Host, Connection, sec-ch-ua, sec-ch-ua-mobile, User-Agent, Accept, ...`. Python requests — `Host, User-Agent, Accept-Encoding, Accept, Connection`. Разный порядок = разный клиент.
- **Accept-Encoding.** Chrome: `gzip, deflate, br, zstd`. requests: `gzip, deflate`. Мелочь, но в сумме с остальным работает.
- **Cookie-поведение.** Реальный браузер шлёт cookie, которые поставил сервер, в определённом порядке. Скрипт часто теряет часть или отправляет в другом порядке.
- **JS-челлендж.** Cloudflare Turnstile, DataDome captcha — если JS не выполнился за N секунд, бан. curl_cffi это не решает, нужен реальный браузер.
Практический чеклист перед запуском парсера
Проверьте свой стек по пунктам. Каждый провал — потенциальный 403.
1. JA3/JA4 совпадает с целевым браузером? Проверить на `tls.peet.ws`.
2. HTTP/2 fingerprint совпадает? Там же.
3. Порядок HTTP-заголовков как у браузера? Смотреть через `mitmproxy` или `tls.peet.ws`.
4. SNI передаётся? `openssl s_client -servername` покажет.
5. ALPN включает `h2`? Без него антибот видит HTTP/1.1-клиент, что подозрительно для «Chrome 120».
6. Тайминги между запросами не меньше 200 мс?
7. Прокси не MITM-ит TLS? Если MITM — сертификат палит вас мгновенно.
Итог
Прокси «умирает» после первого запроса не потому, что IP грязный. Потому что антибот увидел несоответствие между заявленным клиентом и реальным TLS-отпечатком. Меняете IP — не меняете JA3. Меняете JA3 — оживает весь пул.
Правило простое: сначала приведите TLS-стек в порядок (curl_cffi, uTLS, реальный браузер), потом уже крутите прокси. В обратном порядке деньги на прокси уходят в трубу — 200 адресов не спасут от одного палевного ClientHello.