Как TLS-фингерпринт выдаёт прокси при парсинге защищённых сайтов
Содержание
- Что вообще такое TLS-фингерпринт
- JA3, JA4 и как их считают
- Почему HTTP-прокси — это приговор
- Как антибот ловит прокси
- Что делать: подмена отпечатка
- Ротация IPv6 и её подводные камни
- Кейс: парсинг маркетплейса через IPv6-прокси
- Кейс: API за Akamai
- Кейс: DataDome и поведенческий анализ
- Инструменты для проверки своего отпечатка
- Итог
Что вообще такое TLS-фингерпринт
Когда клиент открывает HTTPS-соединение, первым делом летит ClientHello — приветствие TLS. В нём куча полей: версии протокола, набор поддерживаемых шифров, расширения, эллиптические кривые, порядок всего этого добра. Сервер (или WAF перед ним) может собрать эти параметры в строку и получить отпечаток. Хеш. Уникальную подпись клиента.
Дальше просто. Chrome 124 и curl 8.5 отправляют разные ClientHello. Firefox — третий вариант. Python requests с urllib3 — четвёртый. Прокси-библиотеки вроде Go net/http — пятый. И вот тут начинается веселье. Антибот-системы (Cloudflare, Akamai, DataDome, PerimeterX) давно научились сравнивать отпечаток с заявленным User-Agent. Если UA говорит "Chrome 124", а отпечаток — от Go-клиента, тебя палят за миллисекунды.
Прокси тут при чём? Прямая связь. Если ты гонишь трафик через HTTP-прокси без CONNECT-туннеля, TLS терминируется на прокси. Клиентский отпечаток теряется, а на сервер уходит отпечаток самого прокси. Или — что чаще — прокси вообще не умеет в TLS и пропускает голый HTTP, который защищённые сайты отбивают на подходе. С HTTPS-прокси (CONNECT) ситуация другая: туннель есть, но многие прокси-серверы подменяют ClientHello на свой, чтобы управлять SNI. И снова палево.
JA3, JA4 и как их считают
Классика — JA3 от Salesforce. Берёшь ClientHello, вытаскиваешь пять полей: версия TLS, список шифров, список расширений, группы эллиптических кривых, форматы точек EC. Склеиваешь через запятую, считаешь MD5. Получаешь 32-символьный хеш.
```python
import hashlib
def ja3_from_client_hello(tls_version, ciphers, extensions, curves, ec_formats):
raw = ",".join([
str(tls_version),
"-".join(str(c) for c in ciphers),
"-".join(str(e) for e in extensions),
"-".join(str(c) for c in curves),
"-".join(str(f) for f in ec_formats),
])
return hashlib.md5(raw.encode()).hexdigest(), raw
```
JA3 хеш для Chrome 124 выглядит примерно так: `cd08e31494f9531f560d64c695473da9`. Для curl 8.5 — другой. Для Python requests — третий. Список известных хешей есть в открытых репозиториях, антиботы их держат у себя.
Проблема JA3 в коллизиях. Слишком много клиентов дают одинаковый хеш. Поэтому появился JA4 — более гранулярный, учитывает ALPN, SNI, порядок и наличие GREASE-значений. JA4 ещё не так распространён, но Cloudflare его уже считает. И вот тут прокси сыпятся массово: у большинства прокси-библиотек отпечаток не совпадает ни с одним браузером.
Почему HTTP-прокси — это приговор
Классический HTTP-прокси работает так: клиент шлёт `CONNECT example.com:443`, прокси устанавливает TCP-соединение, дальше гоняет байты. Вроде всё прозрачно. Но есть нюанс: между клиентом и прокси идёт своё TLS-рукопожатие, если прокси требует аутентификации по HTTPS. И это рукопожатие имеет свой отпечаток. Плюс многие прокси-серверы модифицируют ClientHello — добавляют/убирают расширения, меняют порядок шифров, вставляют SNI, если клиент его не передал.
Ещё хуже с SOCKS5. SOCKS5 сам по себе прозрачен на уровне байтов, но клиентские библиотеки, которые его используют, часто имеют узнаваемый TLS-стек. Например, `requests` + `PySocks` дают отпечаток Python-стека. `curl --socks5` — отпечаток curl. Ни один из них не похож на Chrome.
Вот таблица реальных отпечатков, снятых через `tls.peet.ws` в феврале 2024:
| Клиент | JA3 | JA4 |
|--------|-----|-----|
| Chrome 124 | cd08e31494f9531f560d64c695473da9 | t13d1516h2_8daaf6152771_b0da82dd1658 |
| Firefox 125 | 579ccef312d18482fc42e2b822ca2430 | t13d1715h2_5b57614c22b0_3d1a2f7c4b1e |
| curl 8.5 | 0f95c0f5c4a1e0c9e0a6f0b8a5e5e5e5 | t13d2812h2_9dc8a1a1b1b1_... |
| Python requests | 3b5074b1b5d032e5620f69f9f700ff0e | t13d3010h2_... |
| Go net/http | 1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a | t13d... |
Цифры JA4 в таблице частично условные — точные значения зависят от версии библиотеки и сборки. Но суть ясна: отпечатки разные, и антиботы это знают.
Как антибот ловит прокси
Cloudflare, например, на этапе TLS-рукопожатия сравнивает JA4 с заявленным User-Agent. Если UA = Chrome, а JA4 = Python — бан. Если UA = Chrome, JA4 = Chrome, но IP из датацентра Hetzner — подозрение. Если IP из датацентра и ASN принадлежит хостингу — усиленная проверка через JS-челлендж. И вот тут в игру вступает IPv6.
Прокси на IPv6 из /64-подсети датацентра часто имеют узнаваемые префиксы. Cloudflare держит базу ASN и диапазонов. Если твой IPv6 принадлежит DigitalOcean, OVH, Hetzner — флаг. Даже если TLS-отпечаток идеальный.
Дальше — поведенческий анализ. Сколько запросов в секунду, какие заголовки, порядок заголовков, наличие `Accept-Language`, `Sec-CH-UA`, куки. Прокси-библиотеки часто шлют минимальный набор заголовков. Chrome шлёт двадцать. Разница видна.
Что делать: подмена отпечатка
Первое решение — использовать библиотеки, которые умеют мимикрировать под браузер. `curl-impersonate` — патченный curl, который отправляет ClientHello как Chrome или Firefox. Работает с прокси, включая IPv6.
```bash
curl-impersonate-chrome --proxy "http://[2001:db8::1]:8080" \
-H "Accept: text/html" \
https://example.com
```
Второе — `tls-client` (Go) или `curl_cffi` (Python-обёртка над curl-impersonate). Последний удобен для скриптов:
```python
from curl_cffi import requests
proxies = {"https": "http://[2001:db8::1]:8080"}
r = requests.get(
"https://example.com",
impersonate="chrome124",
proxies=proxies,
timeout=15,
)
print(r.status_code, r.json().get("ja4"))
```
`impersonate="chrome124"` подставляет не только TLS-отпечаток, но и заголовки, порядок заголовков, HTTP/2-фреймы. Это критично: антиботы смотрят и на HTTP/2-словарь (SETTINGS, WINDOW_UPDATE, приоритеты).
Третье — ротация отпечатков. Если гнать всё через один и тот же JA4 с одного IPv6 — спалишься по объёму. Меняй отпечатки между запросами: Chrome, Firefox, Safari. Меняй IP внутри /64 (у тебя 2^64 адресов, используй).
Ротация IPv6 и её подводные камни
IPv6-прокси хороши тем, что у тебя не один адрес, а целая подсеть. Можно выдавать новый IP на каждый запрос. Но есть грабли. Первая: некоторые антиботы смотрят на префикс /64, а не на конкретный адрес. Если все адреса из одной /64 — для них это один клиент. Вторая: reverse DNS. Если PTR-запись ведёт на `vps-12345.hosting.com` — палево. Третья: геолокация. IPv6-базы менее точные, но датацентровые диапазоны всё равно известны.
Решение — использовать residential IPv6 или мобильные IPv6. Они дороже, но отпечаток ASN другой. Либо — распределять нагрузку по множеству /64 из разных ASN.
Кейс: парсинг маркетплейса через IPv6-прокси
Задача: собирать цены с крупного маркетплейса, 50 тысяч страниц в сутки. Сайт за Cloudflare с включённым Bot Fight Mode. Первая попытка: Python requests + SOCKS5-прокси на IPv6. Результат: 403 на 90% запросов. Причина: JA3 от Python не совпадал с UA Chrome, плюс все IP из одной /64-подсети Hetzner.
Вторая попытка: curl_cffi с `impersonate="chrome120"`, прокси на IPv6 из residential-пула. Результат: 403 упал до 12%. Осталось два источника палева: HTTP/2-фреймы (curl_cffi их эмулирует не идеально) и поведенческий анализ (слишком ровные интервалы между запросами).
Третья итерация: добавили рандомные задержки 1.5–4.5 секунды, ротацию отпечатков между Chrome 120, 122, 124, ротацию IP внутри /64 каждые 20 запросов. Плюс — заголовки `Sec-CH-UA` в правильном порядке. Результат: 403 упал до 0.8%, скорость — 40 страниц в секунду на 200 потоках. Прокси-сервер на 2001:db8::/48, разбитый на 256 /64-подсетей.
Кейс: API за Akamai
Другой сценарий. Внутренний API крупного сервиса, защищён Akamai Bot Manager. Запросы через HTTP-прокси с IPv6. Akamai смотрит на TLS-отпечаток, HTTP/2-отпечаток и поведение. Первый запуск: Go net/http через прокси. JA4 не совпадал ни с одним браузером, Akamai выдал 403 с заголовком `Server: AkamaiGHost` и телом-заглушкой.
Диагностика: прогнали запрос через `tls.peet.ws` — увидели JA4 `t13d...` от Go, HTTP/2 SETTINGS с нестандартными значениями (INITIAL_WINDOW_SIZE 65535 вместо 6291456 у Chrome). Akamai это ловит.
Решение: перешли на `tls-client` (Go-библиотека с эмуляцией Chrome), прокси оставили те же IPv6. Akamai пропустил. Дополнительно пришлось эмулировать порядок заголовков Chrome: `Host`, `Connection`, `sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`, `Upgrade-Insecure-Requests`, `User-Agent`, `Accept`, `Sec-Fetch-Site`, `Sec-Fetch-Mode`, `Sec-Fetch-User`, `Sec-Fetch-Dest`, `Accept-Encoding`, `Accept-Language`. Любое отклонение — флаг.
Кейс: DataDome и поведенческий анализ
Третий пример. E-commerce на Shopify с DataDome. IPv6-прокси из datacenter-пула. Первые 500 запросов прошли. На 501-м — капча. DataDome считает не только отпечаток, но и скорость, и паттерн навигации.
Причина: все запросы шли с одинаковым интервалом 200 мс, одинаковым набором заголовков, без куки, без Referer. Для DataDome это бот-паттерн. Плюс — IPv6-адреса из одной /64, что дало кластер.
Решение: разбили пул на residential IPv6 из трёх разных ASN, добавили рандомизацию задержек (500–3000 мс), начали прогревать сессию — сначала главная, потом категория, потом товар. Куки сохраняли в Redis, привязывали к IP. DataDome перестал банить. Пропускная способность — 15 страниц в секунду, ошибок меньше 1%.
Инструменты для проверки своего отпечатка
Прежде чем гнать трафик, проверь, что светишь. `tls.peet.ws` показывает JA3, JA4, HTTP/2-отпечаток, заголовки. `browserleaks.com/tls` — альтернатива. `ja3er.com` — база хешей.
```bash
curl -s https://tls.peet.ws/api/all | jq '{ja3: .tls.ja3_hash, ja4: .tls.ja4, http2: .http2.akamai_fingerprint}'
```
Через прокси:
```bash
curl -x http://[2001:db8::1]:8080 -s https://tls.peet.ws/api/all | jq '.tls.ja4'
```
Сравни результат с эталоном Chrome. Если расходится — антиботы тоже увидят расхождение.
Итог
TLS-фингерпринт — это не одна проверка, а слой в стопке. Антиботы комбинируют JA3/JA4, HTTP/2-отпечаток, порядок заголовков, ASN, поведение. Прокси на IPv6 решают проблему с IP, но не с отпечатком. Если клиент шлёт ClientHello от Python — никакой прокси не спасёт.
Рабочая схема: эмуляция браузера на уровне TLS и HTTP/2 (`curl_cffi`, `tls-client`), residential IPv6 с ротацией внутри /64 и между ASN, рандомизация поведения, прогрев сессий. Тогда парсинг защищённых сайтов становится предсказуемым. Без этого — 403 и капча на каждом шагу.