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

Почему прокси «умирает» после первого запроса к антибот-системе: разбор поведенческого fingerprint на уровне TLS/JA3

Почему прокси «умирает» после первого запроса к антибот-системе: разбор поведенческого fingerprint на уровне TLS/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.

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