Как TLS fingerprint выдаёт прокси даже при идеальной подмене IP
Содержание
- Что вообще такое JA3 и откуда он берётся
- Почему подмена IP — это ничто
- JA4: что изменилось и почему стало хуже для прокси
- Реальный кейс: curl через SOCKS5 против Cloudflare
- Кейс: Go-бот на своём TLS-стеке
- HTTP/2 fingerprint: следующий слой
- Что делать, если ты на прокси и хочешь не палиться
- Чем эмулировать: обзор инструментов
- Кейс: Playwright headless vs headful
- Почему порядок полей в JA4 всё ещё важен
- Что в итоге
Антибот-системы давно перестали верить заголовкам. User-Agent подделать — минутное дело. IP подменить — прокси за пять баксов. Но есть слой, который большинство «анонимайзеров» игнорирует. TLS handshake. Именно там вылезает всё: клиентское приложение, библиотека, версия, порядок шифров. Cloudflare, Akamai, DataDome, PerimeterX смотрят туда первым делом.
Разберём, как JA3/JA4 ловят прокси-трафик, почему подмена IP не помогает и что реально нужно крутить, чтобы не палиться.
Что вообще такое JA3 и откуда он берётся
JA3 — это хеш от параметров ClientHello. Salesforce придумала его в 2017-м, и с тех пор он стал стандартом де-факто в антибот-индустрии. Идея простая: возьми поля TLS ClientHello, склей в строку, посчитай MD5. Получишь отпечаток, который у стабильной библиотеки не меняется от сессии к сессии.
Что входит в строку:
- версия TLS (771 для TLS 1.2, 772 для 1.3)
- список cipher suites в порядке следования
- список extensions
- список elliptic curves
- формат точек эллиптических кривых
Порядок критичен. Chrome 120 отправляет шифры в одном порядке, Firefox 121 — в другом, curl 8.x — в третьем, python-requests — вообще в четвёртом. Даже патч-версия библиотеки может поменять порядок. OpenSSL 3.0 и 3.2 дают разные JA3.
Вот как выглядит строка до хеширования для типичного curl:
```
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
```
Хеш от неё — `3b5074b1b5d032e5620f69f9f700ff0e`. Это curl на OpenSSL. Любой антибот, увидевший такой JA3 от «Chrome на Windows», сразу понимает: врут.
Почему подмена IP — это ничто
Прокси меняет сетевой слой. NAT, туннель, SOCKS5 — неважно. Пакеты уходят с другого адреса, но содержимое TLS ClientHello остаётся клиентским. Антибот видит: IP из датацентра Hetzner, а JA3 — curl. Или IP резиденшла в Милане, а JA3 — Go net/http. Несоответствие.
Дальше включается корреляция. Cloudflare, к примеру, держит базу из миллиардов реальных handshake'ов с браузеров. Если твой JA3 совпадает с Chrome 120, но IP-адрес принадлежит подсети DigitalOcean — это уже сигнал. А если с этого IP за последние сутки было 4000 запросов с пятью разными JA3 — сигнал превращается в приговор.
Есть ещё вторая часть — JA3S, отпечаток ServerHello. Он показывает, какой сервер ответил. Иногда антиботы сравнивают пару JA3/JA3S с известными комбинациями. Если клиент представляется Chrome, а сервер отвечает как nginx с дефолтным OpenSSL — странно, но бывает. А вот если клиент говорит «я Chrome», а JA3S не совпадает с тем, что этот Chrome обычно получает от Cloudflare — уже подозрительно.
JA4: что изменилось и почему стало хуже для прокси
JA4 появился в 2023-м как замена JA3. FoxIO переработала формат. Вместо MD5 — читаемая строка с префиксами. Пример:
```
t13d1516h2_8daaf6152771_02713d6af862
```
Расшифровка: `t13` — TLS 1.3, `d` — domain (SNI есть), `15` — 15 шифров, `16` — 16 extensions, `h2` — ALPN HTTP/2. Дальше два хеша — шифры и extensions. Плюс отдельно считается JA4H (HTTP-заголовки в порядке), JA4L (latency), JA4T (TCP fingerprint).
Ключевое отличие: JA4 сортирует шифры и extensions перед хешированием. JA3 зависел от порядка, JA4 — нет. Зато JA4 добавил больше полей и стал устойчивее к мелким изменениям. Плюс появились подверсии — JA4_o (original order), JA4_r (raw).
Для прокси это означает: подделать JA4 сложнее, чем JA3. Раньше хватало переставить шифры в списке. Теперь надо эмулировать весь набор — включая ALPN, SNI-поведение, GREASE values, порядок extensions.
Реальный кейс: curl через SOCKS5 против Cloudflare
Проблема: парсер на Python с requests через SOCKS5-прокси получает 403 от Cloudflare на 80% запросов. Прокси — резидентный, IP чистый, geolocation совпадает с целевым сайтом.
Причина: requests использует urllib3 + OpenSSL. JA3 этой связки — `3b5074b1b5d032e5620f69f9f700ff0e` (для OpenSSL 1.1.1) или `3f7c3e...` для 3.0. Cloudflare видит: клиент заявляет Chrome 120 в User-Agent, но TLS-отпечаток — Python. Классика.
Технические детали: requests по умолчанию не отправляет ALPN h2 — только http/1.1. Chrome всегда шлёт h2. В JA4 это поле `h1` вместо `h2` — мгновенный маркер. Плюс у requests отсутствует extension 17513 (application_settings), который Chrome отправляет с версии 110. Плюс порядок шифров отличается от Chrome.
Решение: заменить requests на curl_cffi с импersonation.
```python
from curl_cffi import requests
r = requests.get(
"https://target.example/api/data",
impersonate="chrome120",
proxies={"https": "socks5://user:pass@proxy:1080"},
timeout=15
)
print(r.status_code, r.json())
```
curl_cffi использует BoringSSL и эмулирует реальный ClientHello Chrome. JA4 совпадает с настоящим браузером с точностью до подверсии. 403 ушли, success rate вырос с 20% до 94% на том же пуле прокси.
Кейс: Go-бот на своём TLS-стеке
Проблема: сервис на Go (net/http) собирает данные с маркетплейса. Прокси — пул из 200 адресов, ротация каждые 10 запросов. Через сутки все IP попали в бан.
Причина: Go crypto/tls даёт стабильный JA3 `6734f37431670b3ab4292b8f60f29984`. Он не совпадает ни с одним браузером. Антибот маркетплейса (DataDome) увидел 200 IP с одинаковым нечеловеческим отпечатком — и заблокировал всю подсеть по ASN.
Технические детали: Go по умолчанию не шлёт GREASE, использует другой набор extensions, порядок шифров фиксирован в исходниках crypto/tls. User-Agent менялся на разные — Chrome, Firefox, Safari. Но JA3 оставался один. DataDome строит граф «IP → JA3» и ловит аномалии по частоте.
Решение: обернуть HTTP-клиент в uTLS — библиотеку от refraction-networking, которая позволяет вручную собрать ClientHello.
```go
package main
import (
"fmt"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/proxy"
)
func main() {
dialer, _ := proxy.SOCKS5("tcp", "proxy:1080", nil, proxy.Direct)
cfg := &utls.Config{ServerName: "target.example"}
rawConn, _ := dialer.Dial("tcp", "target.example:443")
uconn := utls.UClient(rawConn, cfg, utls.HelloChrome_Auto)
if err := uconn.Handshake(); err != nil {
panic(err)
}
tr := &http.Transport{
DialTLS: func(_, _ string) (net.Conn, error) { return uconn, nil },
}
client := &http.Client{Transport: tr, Timeout: 20 * time.Second}
resp, err := client.Get("https://target.example/api")
fmt.Println(resp.StatusCode, err)
}
```
После перехода на uTLS с `HelloChrome_Auto` success rate вырос с 0% до 71% за сутки. Оставшиеся 29% — это уже поведенческий анализ, не TLS.
HTTP/2 fingerprint: следующий слой
JA3/JA4 — только начало. Если клиент прошёл TLS-проверку, антибот смотрит на HTTP/2-фреймы. У каждого клиента свой порядок SETTINGS, свой window size, свои приоритеты потоков. Akamai называет это HTTP/2 fingerprint, иногда — Akamai fingerprint.
Chrome отправляет SETTINGS с параметрами: HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0, MAX_CONCURRENT_STREAMS=1000, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144. Порядок именно такой. Go net/http шлёт другой набор и в другом порядке. Python httpx — третий.
Совпадение JA4 + HTTP/2 fingerprint — уже серьёзный признак реального браузера. Расхождение — красный флаг. curl_cffi и uTLS эмулируют оба слоя. Голый requests — ни одного.
Что делать, если ты на прокси и хочешь не палиться
Стратегия зависит от задачи. Если это разовый парсинг — хватит curl_cffi с импersonation Chrome или Firefox. Если масштабный сбор с ротацией IP — нужен полный стек: реалистичный JA4, HTTP/2 fingerprint, консистентные заголовки, правильные тайминги.
Прокси-пул сам по себе ничего не решает. Более того, дешёвые пулы часто усугубляют: тысяча IP с одним JA3 — это хуже, чем один IP с правильным отпечатком. Антиботы давно научились коррелировать. Если хочешь серьёзный пул с чистыми адресами и без общей истории — смотри в сторону сервисов вроде lexic.ml, где IPv6-подсети выдаются с 2015 года и не засвечены в массовых бан-листах. Но даже с хорошими IP без правильного TLS-стека ты просто быстрее получишь 403.
Проверять свой отпечаток можно через tls.browserleaks.com или ja3er.com. Открой сайт из браузера, потом из своего скрипта — сравни JA3/JA4. Если не совпало с целевым браузером, антибот это увидит тоже.
Чем эмулировать: обзор инструментов
| Инструмент | Язык | JA3/JA4 | HTTP/2 fp | Поддержка прокси |
|---|---|---|---|---|
| curl_cffi | Python | да (Chrome, Firefox, Safari) | да | SOCKS5, HTTP |
| uTLS | Go | да (ручная сборка) | частично | через dialer |
| tls-client | Go | да (готовые профили) | да | SOCKS5, HTTP |
| requests + httpx | Python | нет | нет | да |
| curl (CLI) | — | фиксированный | нет | да |
| Playwright | Python/Node | да (реальный браузер) | да | да |
Playwright — самое надёжное, но дорогое по ресурсам. Один экземпляр Chromium жрёт 200-400 МБ RAM. Для 50 параллельных сессий нужен сервер на 32 ГБ. curl_cffi в том же сценарии — 200 МБ на всё.
Кейс: Playwright headless vs headful
Проблема: антибот крупного ритейлера блокирует Playwright headless на 100% запросов, хотя IP чистые, а прокси резидентные.
Причина: headless Chrome имеет другой JA3, чем headful. Плюс в headless отсутствует часть extensions, которые включает GPU-рендеринг. Плюс WebGL-рендерер в headless — SwiftShader, а не реальная видеокарта. Cloudflare Turnstile проверяет всё это вместе. JA3 headless Chrome 120 — `1d7f5b...`, headful — `3f7c3e...`. Разные.
Технические детали: в headless-режиме Chrome отключает extension 27 (compress_certificate) и меняет порядок 43 (supported_versions). Плюс ALPN может отличаться. Turnstile ловит это на этапе TLS, ещё до загрузки страницы.
Решение: запуск с `--headless=new` (новый headless с 2023-го) плюс маскировка под headful через playwright-stealth и подмена WebGL.
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
headless=False,
args=[
"--disable-blink-features=AutomationControlled",
"--use-gl=angle",
"--use-angle=gl",
],
proxy={"server": "socks5://proxy:1080"},
)
ctx = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
)
page = ctx.new_page()
page.goto("https://target.example")
```
На практике headless=False требует Xvfb на сервере. Работает стабильнее, чем любые трюки с новым headless. Success rate вырос с 0% до 96%.
Почему порядок полей в JA4 всё ещё важен
JA4 сортирует шифры и extensions — это защита от мелких мутаций. Но остаются поля, которые не сортируются: ALPN, SNI-поведение, версия TLS. Если клиент заявляет TLS 1.3, но отправляет только TLS 1.2-шифры — несоответствие. Если SNI отсутствует при подключении к HTTPS-хосту по домену — подозрительно.
Антиботы строят матрицу «JA4 × поведение». Один и тот же JA4 с разными User-Agent — норм (у Chrome много версий). Один JA4 с разными Accept-Language и разными часовыми поясами — уже странно. Пять JA4 с одного IP за минуту — почти наверняка автоматизация.
Что в итоге
TLS fingerprint — это не одна проверка, а слой. JA3 устарел, JA4 пришёл на смену, HTTP/2 fingerprint добавился сверху. Прокси решает только сетевую часть. Если клиентский стек не совпадает с заявленным браузером — никакой пул IP не спасёт.
Порядок действий для любого парсера: сначала проверь свой JA4 на browserleaks, потом сравни с целевым браузером, потом подними curl_cffi или uTLS с нужным профилем, и только после этого думай про прокси. Обратный порядок — это тот самый зоопарк из 200 IP с одним отпечатком Go, который банится за сутки.
И последнее. Антиботы обновляются каждые несколько недель. Профиль, который работал в марте, в мае может палиться. Держи в голове, что эмуляция — это гонка, а не разовое решение.