Как антифрод выявляет смену прокси по TLS JA3/JA4 отпечатку и что это значит для мультиаккаунтинга
Содержание
Что вообще такое JA3 и JA4
TLS-рукопожатие — это не просто обмен сертификатами. Клиент в ClientHello отправляет кучу параметров: версии TLS, список поддерживаемых шифров (cipher suites), список расширений, эллиптические кривые, форматы точек. Порядок и состав этих полей зависит от конкретной библиотеки и её версии. OpenSSL 3.0 собирает ClientHello иначе, чем BoringSSL из Chrome 120, и уж совсем иначе, чем Python `requests` с его urllib3.
JA3 — это MD5-хеш от конкатенации пяти полей ClientHello: версия TLS, cipher suites, extensions, elliptic curves, ec_point_formats. Формат такой:
```
TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
```
Считается как `md5("771,4865-4866-4867-49195-49199,...,0-11-10-35-22-23-13-43-45-51,29-23-24-25,0")`. Получается 32-символьная строка. Сервер её видит ещё до того, как клиент отправит хоть один HTTP-запрос.
JA4 — наследник, появился в 2023. Вместо MD5 использует SHA256 (усечённый), формат человекочитаемый:
```
t13d1516h2_8daaf6152771_02713d6af862
```
Здесь `t13` — TLS 1.3, `d` — domain (SNI присутствует), `15` — число cipher suites, `16` — число extensions, `h2` — ALPN. Дальше два хеша. JA4 устойчивее к рандомизации порядка расширений (GREASE), которую Chrome начал применять.
Почему отпечаток вообще работает против прокси
Прокси меняет IP. Иногда — страну, ASN, часовой пояс через GeoIP. Но TLS-стек остаётся тем же. Если вы сидите на `curl 7.81` через SOCKS5-прокси, антифрод видит JA3 от curl, а не от браузера, которым вы якобы пользуетесь.
Классическая схема палева: пользователь заходит на сайт, заявляет User-Agent Chrome 121, но JA3 соответствует Python requests. Мгновенный флаг. Cloudflare, Akamai, DataDome, PerimeterX — все они давно держат базы JA3/JA4 и сверяют. Cloudflare называет это "Bot Management", Akamai — "Bot Manager Premier". Разница в цене, но принцип один.
Числа: по данным Cloudflare за 2023, доля запросов с несоответствием UA-отпечатку превышает 40% в трафике, помеченном как "automated". Это не значит, что все они блокируются — но попадают в отдельную корзину для скоринга.
Как выглядит пайплайн детекции
На стороне антифрода это выглядит так. Nginx или HAProxy терминирует TLS, вытаскивает ClientHello и считает JA3/JA4. Дальше идёт lookup в Redis или ClickHouse: есть ли такой отпечаток в базе легитимных браузеров. Если да — вес низкий. Если нет — вес растёт.
Параллельно собирается вторая линия сигналов: HTTP/2 fingerprint (SETTINGS frame, WINDOW_UPDATE, приоритеты), порядок заголовков, наличие `sec-ch-ua`, `sec-fetch-*`, Accept-Language, cookies. Всё это складывается в единый скоринг. JA3 — только один из ~50 признаков.
Пример на nginx с модулем `nginx-ssl-ja3` (форк от fooinha):
```nginx
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
set \$ja3 "";
if (\$http_user_agent ~* "curl|python|wget") {
set \$ja3 "suspicious";
}
location / {
proxy_set_header X-JA3 \$ssl_ja3;
proxy_set_header X-JA3-Hash \$ssl_ja3_hash;
proxy_pass http://backend;
}
}
```
На бэкенде (Python + FastAPI) считаем скор:
```python
from fastapi import FastAPI, Request
import hashlib
KNOWN_BROWSER_JA3 = {
"cd08e31494f9531f560d64c695473da9": "chrome_120",
"b32309a26951912be7dba376398abc3b": "firefox_121",
"3b5074b1b5d032e5620f69f9f700ff0e": "safari_17",
}
app = FastAPI()
@app.middleware("http")
async def ja3_check(request: Request, call_next):
ja3 = request.headers.get("X-JA3-Hash", "")
ua = request.headers.get("user-agent", "")
score = 0
if ja3 not in KNOWN_BROWSER_JA3:
score += 40
if "Chrome" in ua and KNOWN_BROWSER_JA3.get(ja3) != "chrome_120":
score += 30
request.state.risk = score
response = await call_next(request)
response.headers["X-Risk-Score"] = str(score)
return response
```
Порог обычно 50-60. Выше — капча. Выше 80 — блок.
Что это значит для мультиаккаунтинга
Если вы держите 50 аккаунтов и все они ходят через один и тот же прокси-пул с одного и того же TLS-стека — антифрод видит 50 сессий с идентичным JA3. Это норма для одного пользователя (у него один браузер), но не норма, если IP-адреса разные и география разная. Связка "JA3 + смена IP" — сильный сигнал фрода.
Обратная ситуация тоже палево: один аккаунт, но JA3 скачет между Chrome, Firefox и curl. Реальный пользователь не переключает браузер каждые 5 минут.
Прокси-сервисы типа lexic.ml дают разные IPv6-адреса, но не меняют ваш TLS-стек. Если вы используете curl или requests через их SOCKS5, отпечаток остаётся curl/requests. Для мультиаккаунтинга это означает: прокси решает проблему IP-репутации, но не решает проблему отпечатка. Нужен либо реальный браузер (Playwright, Selenium с патчами), либо библиотека с подменой JA3.
Подмена JA3 на практике
Самый рабочий путь — `curl-impersonate`. Это форк curl, который собирает ClientHello идентично Chrome или Firefox. Установка:
```bash
git clone https://github.com/lwthiker/curl-impersonate.git
cd curl-impersonate
make build
./curl_chrome116 -x socks5://user:pass@proxy.example.com:1080 \
-H "Accept-Language: en-US,en;q=0.9" \
https://httpbin.org/headers
```
Проверить свой JA3 можно на `https://tls.browserleaks.com/json`. Там же покажет JA4. Если `curl_chrome116` выдаёт JA3, совпадающий с реальным Chrome 116 — вы в базе легитимных.
Для Python есть `curl_cffi`:
```python
from curl_cffi import requests
r = requests.get(
"https://tls.browserleaks.com/json",
impersonate="chrome120",
proxies={"https": "socks5://user:pass@proxy:1080"}
)
print(r.json()["ja3_hash"])
```
`impersonate` принимает значения `chrome99`...`chrome124`, `firefox133`, `safari17_0`. Под капотом — тот же curl-impersonate, обёрнутый в C-биндинги.
Где всё ломается
Первое: HTTP/2 fingerprint. Даже если JA3 совпадает с Chrome, порядок SETTINGS-фреймов и приоритеты могут отличаться. Антифроды вроде Akamai смотрят на это отдельно. `curl_cffi` умеет подделывать и его, но не идеально.
Второе: TCP/IP fingerprint. TTL, window size, порядок опций TCP. Если вы идёте через прокси, TCP-стек — это стек прокси-сервера, не ваш. Прокси на Linux с дефолтными настройками выдаёт один fingerprint, а заявленный Windows-браузер — другой. p0f и ему подобные это ловят.
Третье: поведенческие сигналы. Скорость заполнения форм, движения мыши (если есть JS-трекинг), время между запросами. JA3 тут ни при чём, но в общем скоре учитывается.
Четвёртое: сертификаты и ALPN. Если ваш клиент не поддерживает ALPN `h2`, а Chrome поддерживает — расхождение. Проверяется тривиально.
Разбор реальных ситуаций
Пример: сервер на nginx 1.24 с терминированием TLS и логированием JA3 через `ssl_ja3_hash` показал, что из 12 000 запросов за сутки 3 400 имели JA3, не входящий в топ-1000 браузерных. Из них 2 800 использовали User-Agent Chrome. Все 2 800 пришли с 47 IP-адресов, принадлежащих одному /24. Скор выше 70 у 94% из них. Итог — блок на уровне WAF, 0.3% ложных срабатываний (проверяли вручную выборку).
Другой случай: клиент жаловался, что аккаунты в сервисе X банятся через 2-3 дня. Логи показали: все аккаунты ходили с одного JA3 (Python requests 2.31), но с разных IPv6. Антифрод связал их по отпечатку и забанил всю группу одним махом. Решение — переход на `curl_cffi` с `impersonate="chrome120"` и рандомизацией между Chrome/Firefox/Safari. Выживаемость выросла с 3 дней до 40+.
Третий: SaaS-сервис с Cloudflare Bot Management. Запросы через residential-прокси с curl проходили капчу 1 из 10 раз. После перехода на `curl-impersonate` с флагом `--http2` — 8 из 10. Оставшиеся 2 — это уже поведенческий скоринг и rate limiting, не TLS.
Что делать, если вы на другой стороне
Если вы строите антифрод и хотите ловить такие схемы — не полагайтесь только на JA3. Chrome с рандомизацией GREASE выдаёт разные JA3 при каждом запуске, если считать по сырым полям. JA4 это учитывает, JA3 — нет. Считайте оба, плюс HTTP/2 fingerprint, плюс TCP/IP. Храните историю отпечатков по аккаунту: если за неделю у одного пользователя сменилось 5 разных JA3 — это не пользователь, это ферма.
Порог для блокировки подбирайте эмпирически. На тестовом трафике в 100 000 сессий с известной разметкой (легитим vs бот) оптимальный порог обычно даёт precision 0.92-0.96 и recall 0.85-0.90. Выше precision — растут ложные блокировки реальных пользователей, что дороже, чем пропущенный бот.
Итог по цифрам
JA3-хеш считается за ~2 микросекунды на запрос. JA4 — за ~5, из-за SHA256. Lookup в Redis — 0.5-1 мс. Полный пайплайн детекции на одном узле держит 10-15 тысяч RPS без деградации. Для 100 тысяч RPS нужен шардинг по хешу отпечатка и отдельный ClickHouse для аналитики.
Мультиаккаунтинг без подмены TLS-отпечатка живёт ровно до первого серьёзного антифрода. Прокси меняет IP, но не меняет то, как вы стучитесь в дверь. Если стучитесь как робот — откроют не сразу, а сначала посмотрят в глазок.