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

Как TLS fingerprint выдаёт прокси даже при идеальной подмене IP

Как TLS fingerprint выдаёт прокси даже при идеальной подмене IP

Антибот-системы давно перестали верить заголовкам. 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, который банится за сутки.

И последнее. Антиботы обновляются каждые несколько недель. Профиль, который работал в марте, в мае может палиться. Держи в голове, что эмуляция — это гонка, а не разовое решение.

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